diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b0e5d763 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +.idea +/.idea/.gitignore +/.idea/compiler.xml +/.idea/jarRepositories.xml +/.idea/misc.xml +/.idea/modules.xml +/.idea/uiDesigner.xml +/.idea/vcs.xml +codebuild_build.sh +artifacts/ +target/ +/mqtt-sn-gateway-vertx/mqtt-sn-gateway-vertx.iml +/mqtt-sn-gateway-artefact/mqtt-sn-gateway-artefact.iml +/mqtt-sn-gateway/mqtt-sn-gateway.iml +/mqtt-sn-core-sec/mqtt-sn-core-sec.iml +/mqtt-sn-core/mqtt-sn-core.iml +/mqtt-sn-codec-netty/mqtt-sn-codec-netty.iml +/mqtt-sn-codec/mqtt-sn-codec.iml +/mqtt-sn-client/mqtt-sn-client.iml +/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/TMain.java +/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/LMain.java +/mqtt-publisher/ +/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/LTCPMain.java +/mqtt-sn-core/ext/my-keystore.jks +/mqtt-sn-core/ext/my-truststore.jks +/mqtt-sn-gateway-artefact/src/main/java/org/slj/mqtt/sn/gateway/impl/TcpGatewayMain.java +mqtt-sn-core-sec/ +mqtt-sn-client-sec/ +/client.properties +/config.properties +/gateway.properties diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..7401a313 --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ + +# MQTT For Small Things (SN) +MQTT-SN is an optimized version of the MQTT specification designed for use on small, low powered, sensor devices, often running on the edge of the network; typical of the IoT. + +View the intial [MQTT-SN Version 1.2](http://www.mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf) specification written by **Andy Stanford-Clark** and **Hong Linh Truong** from **IBM**. + +### MQTT-SN Evolved +As of late 2020, the MQTT technical committee at OASIS (via a sub-committee led by **Ian Craggs** ([Ian's Blog](https://modelbasedtesting.co.uk))) are working on standardisation and changes to bring MQTT-SN more in line with MQTT version 5. +This is an ongoing piece of work which we hope to formalise and conclude in 2021. + +### Project Goals +Noteable open-source works already exists for various MQTT and MQTT-SN components, the mains ones of note are listed below; many fall under the eclipse PAHO project. The work by **Ian Craggs** et al on the MQTT-SN Java gateway set out the wire transport implementation and a reference transparent gateway. That is a gateway which connects a client to a broker side socket and mediates the access. My goal of this project and its work are that it should provide an open-source **aggregating gateway** implementation, and should implement the wire messages such that the next interation of MQTT-sn can be demonstrated using this project. + +### MQTT / MQTT-SN differences +The SN variant of MQTT is an expression of the protocol using smaller messages, and an optimised message lifecycle. All the features of a core MQTT broker are available to the SN clients, with the gateway implementation hiding the complexities of the protocol using various multiplexing techniques. An SN client has no need of a TCP/IP connection to a broker, and can choose to any transport layer; for example UDP, BLE, Zigbee etc. + +### Project modules +Module | Language & Build | Dependencies | Description +------------ | ------------- | ------------- | ------------- +[mqtt-sn-codec](/mqtt-sn-codec) | Java 1.8, Maven | **Mandatory** | Pure java message parsers and writers. Includes interfaces and abstractions to support future versions of the protocol. +[mqtt-sn-core](/mqtt-sn-core) | Java 1.8, Maven | **Mandatory** | Shared interfaces and abstractions for use in the various MQTT-SN runtimes +[mqtt-sn-core-sec](/mqtt-sn-core-sec) | Java 1.8, Maven | Optional | DTLS implementation of the transport layer to add a secure datagram socket to a broker & client runtime **NOTE: This project is still work in progress will in the short term fail compilation** +[mqtt-sn-client](/mqtt-sn-client) | Java 1.8, Maven | Client | A lightweight client with example transport implementations. Exposes both a simple blocking API and an aysnc publish API to the application and hides the complexities of topic registrations and connection management. +[mqtt-sn-gateway](/mqtt-sn-gateway) | Java 1.8, Maven | Gateway | The core gateway runtime. The end goal is to provide all 3 variants of the gateway (Aggregating, Transparent & Forwarder) where possible. I have started with the aggregating gateway, since this is the most complex, and the most suitable for larger scale deployment. +[mqtt-sn-gateway-artefact](/mqtt-sn-gateway-artefact) | Java 1.8, Maven | Optional | Simple runtime implementation of UDP aggregating gateway with a shaded jar build +[mqtt-sn-codec-netty](/mqtt-sn-codec-netty) | Java 1.8, Maven | Optional | Binding of the codecs into Netty codecs for use in a Netty runtime + +### Quick start - Gateway + +Git checkout the repository. For a simple standalone jar execution, run the following maven deps. + +```shell script +mvn -f mqtt-sn-codec clean install +mvn -f mqtt-sn-core clean install +mvn -f mqtt-sn-gateway clean install +mvn -f mqtt-sn-gateway-artefact clean package +``` + +This will yield a file in your mqtt-sn-gateway-artefact/target directory that will be called mqtt-sn-gateway-.jar. You can then start a broker +from a command line using; + +```shell script +java -jar /mqtt-sn-gateway-.jar >> /mqtt-sn-gateway-udp.log 2>&1 +``` + +Running the executable jar will run the code specified in AggregatingGatewayMain. + +Click into [mqtt-sn-gateway-artefact](/mqtt-sn-gateway-artefact) for more details on the gateway. + + +### Quick start - Client + +Ensure you have the requisite projects mounted per the table above. You can then run the main method for in the Example.java located in the project. You can see the configuration options for details +on how to customise your installation. + +Click into [mqtt-sn-client](/mqtt-sn-client) for more details on the client. + +### Configuration + +The default client/gateway behaviour can be customised using configuration options. Sensible defaults have been specified which allow it to all work out of the box. +Many of the options below are applicable for both the client and gateway runtimes. + +Options | Default Value | Type | Description +------------ | ------------- | ------------- | ------------- +contextId | NULL | String | This is used as either the clientId (when in a client runtime) or the gatewayId (when in a gateway runtime). **NB: This is a required field and must be set by the application.** +maxWait | 10000 | int | Time in milliseconds to wait for a confirmation message where required. When calling a blocking method, this is the time the method will block until either the confirmation is received OR the timeout expires. +maxTopicLength | 1024 | int | Maximum number of characters allowed in a topic including wildcard and separator characters. +threadHandoffFromTransport | true | boolean | Should the transport layer delegate to and from the handler layer using a thread hand-off. **NB: Depends on your transport implementation as to whether you should block.** +handoffThreadCount | 5 | int | How many threads are used to process messages recieved from the transport layer +discoveryEnabled | false | boolean | When discovery is enabled the client will listen for broadcast messages from local gateways and add them to its network registry as it finds them. +maxTopicsInRegistry | 128 | int | Max number of topics which can reside in the CLIENT registry. This does NOT include predefined alias's. +msgIdStartAt | 1 | int (max. 65535) | Starting number for message Ids sent from the client to the gateways (each gateway has a unique count). +aliasStartAt | 1 | int (max. 65535) | Starting number for alias's used to store topic values (NB: only applicable to gateways). +maxMessagesInflight | 1 | int (max. 65535) | In theory, a gateway and broker can have multiple messages inflight concurrently. The spec suggests only 1 confirmation message is inflight at any given time. (NB: do NOT change this). +maxMessagesInQueue | 100 | int | Max number of messages allowed in a client's queue. When the max is reached any new messages will be discarded. +requeueOnInflightTimeout | true | boolean | When a publish message fails to confirm, should it be requeued for DUP sending at a later point. +predefinedTopics | Config| Map | Where a client or gateway both know a topic alias in advance, any messages or subscriptions to the topic will be made using the predefined IDs. +networkAddressEntries | Config | Map | You can prespecify known locations for gateways and clients in the network address registry. NB. The runtime will dynamically update the registry with new clients / gateways as they are discovered. In the case of clients, they are unable to connect or message until at least 1 gateway is defined in config OR discovered. +sleepClearsRegistrations | true | boolean | When a client enters the ASLEEP state, should the NORMAL topic registered alias's be cleared down and reestablished during the next AWAKE or ACTIVE states. +minFlushTime | 1000 | int | Time in milliseconds between a gateway device last receiving a message before it begins processing the client queue +discoveryTime | 3600 | int | The time (in seconds) a client will wait for a broadcast during CONNECT before giving up +pingDivisor | 4 | int | The divisor to use for the ping window, the dividend being the CONNECT keepAlive resulting in the quotient which is the time (since last sent message) each ping will be issued +maxProtocolMessageSize | 1024 | int | The max allowable size (in bytes) of protocol messages that will be sent or received by the system. **NB: this differs from transport level max sizes which will be determined and constrained by the MTU of the transport** +### Runtime + +You can hook into the runtime and provide your own implementations of various components or bind in listeners to give you control or visibility onto aspects of the system. + +#### Traffic Listeners + +You can access all the data sent to and from the transport adapter by using traffic listeners. + +```java + MqttsnClientRuntimeRegistry.defaultConfiguration(options). + withTransport(new MqttsnClientUdpTransport(udpOptions)). + withTrafficListener(new IMqttsnTrafficListener() { + @Override + public void trafficSent(INetworkContext context, byte[] data, IMqttsnMessage message) { + System.err.println(String.format("message [%s]", message)); + } + + @Override + public void trafficReceived(INetworkContext context, byte[] data, IMqttsnMessage message) { + System.err.println(String.format("message [%s]", message)); + } + }). + withCodec(MqttsnCodecs.MQTTSN_CODEC_VERSION_1_2); +``` + +### Related people & projects +Our goal on the [MQTT-SN technial committee](https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=mqtt) is to drive and foster a thriving open-source community. Listed here are some related open-source projects with some comments. + +Project | Author | Link | Description +------------ | ------------- | ------------- | ------------- +Paho Mqtt C Client | Various | [GitHub Repository](https://github.com/eclipse/paho.mqtt.c) |Fully featured MQTT C client library +Paho Mqtt C Embedded | Various | [GitHub Repository](https://github.com/eclipse/paho.mqtt.embedded-c) | Fully featured embedded MQTT C client library +Paho Mqtt-Sn C Embedded | Various | [GitHub Repository](https://github.com/eclipse/paho.mqtt-sn.embedded-c) | C implementation of a transparent MQTT-SN gateway, client and codecs +Mqtt-sn Transparent Java Gateway | Ian Craggs & jsaak | [GitHub Repository](https://github.com/jsaak/mqtt-sn-gateway) | Java implementation of a transparent MQTT-SN gateway, c + +### Aggregating gateway diagram + +![MQTT-SN Aggregating Gateway Architecture](/images/mqttsn-arch.png) + diff --git a/appspec.yml b/appspec.yml new file mode 100644 index 00000000..97980927 --- /dev/null +++ b/appspec.yml @@ -0,0 +1,18 @@ +version: 0.0 +os: linux +files: + - source: / + destination: /home/ubuntu/mqtt-sn-gateway-udp +hooks: + ApplicationStop: + - location: stop.sh + timeout: 30 + runas: root + AfterInstall: + - location: create_run_script.sh + timeout: 30 + runas: ubuntu + ApplicationStart: + - location: start.sh + timeout: 30 + runas: root diff --git a/buildspec.yml b/buildspec.yml new file mode 100644 index 00000000..77a12637 --- /dev/null +++ b/buildspec.yml @@ -0,0 +1,21 @@ +version: 0.2 + +phases: + install: + runtime-versions: + java: openjdk8 + build: + commands: + - echo Build started on `date` + - mvn -f mqtt-sn-codec clean install + - mvn -f mqtt-sn-core clean install + - mvn -f mqtt-sn-gateway clean install + + - mvn -f mqtt-sn-gateway-artefact clean package + +artifacts: + files: + - mqtt-sn-gateway-artefact/target/mqtt-sn-gateway-1.0.0.jar + - ec2-scripts/* + - appspec.yml + discard-paths: yes diff --git a/ec2-scripts/create_run_script.sh b/ec2-scripts/create_run_script.sh new file mode 100644 index 00000000..900109d5 --- /dev/null +++ b/ec2-scripts/create_run_script.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +export mqtt_broker=$(aws ssm get-parameter --name /MQTT_SN_Gateway/iots/mqtt_broker --region eu-west-1 | jq -r '.Parameter.Value') +export thing_id=$(aws ssm get-parameter --name /MQTT_SN_Gateway/iots/thing_id --with-decryption --region eu-west-1 | jq -r '.Parameter.Value') +export thing_user=$(aws ssm get-parameter --name /MQTT_SN_Gateway/iots/thing_user --with-decryption --region eu-west-1 | jq -r '.Parameter.Value') +export thing_password=$(aws ssm get-parameter --name /MQTT_SN_Gateway/iots/thing_password --with-decryption --region eu-west-1 | jq -r '.Parameter.Value') + +cd "$(dirname "${BASH_SOURCE[0]}")" +envsubst '${mqtt_broker},${thing_id},${thing_user},${thing_password}' < run.sh.template > /home/ubuntu/mqtt-sn-gateway-udp/run.sh +chmod +x /home/ubuntu/mqtt-sn-gateway-udp/run.sh diff --git a/ec2-scripts/run.sh.template b/ec2-scripts/run.sh.template new file mode 100644 index 00000000..e851aa0d --- /dev/null +++ b/ec2-scripts/run.sh.template @@ -0,0 +1,33 @@ +#!/bin/bash + +export MQTTSN_HOME=/home/ubuntu/mqtt-sn-gateway-udp + +# Get system RAM in KB +totalMemKB=$(awk '/MemTotal:/ { print $2 }' /proc/meminfo) + +# Set default memFlags just in case... +memFlags="-Xmx128M -Xms64M" + +# t3.nano (512MB RAM) +if ((${totalMemKB}<=512000)); then + memFlags="-Xmx128M -Xms64M" +# t3.micro (1GB RAM) +elif ((512001<=${totalMemKB} && ${totalMemKB}<=1024000)); then + memFlags="-Xmx512M -Xms128M" +# t3.small (2GB RAM) +elif ((1024001<=${totalMemKB} && ${totalMemKB}<=2048000)); then + memFlags="-Xmx1G -Xms256M" +# t3.medium (4GB RAM) +elif ((2048001<=${totalMemKB} && ${totalMemKB}<=4096000)); then + memFlags="-Xmx2G -Xms256M" +# t3.large and above (8GB+ RAM) +elif ((${totalMemKB}>=4096001)); then + memFlags="-Xmx2G -Xms256M" +fi + +echo "System RAM KB: ${totalMemKB}" +echo "Setting memFlags: ${memFlags}" + +cd $MQTTSN_HOME + +/usr/bin/java ${memFlags} -jar /home/ubuntu/mqtt-sn-gateway-udp/mqtt-sn-gateway-1.0.0.jar 2442 ${thing_id} ${mqtt_broker} 1883 ${thing_user} ${thing_password} >> /home/ubuntu/mqtt-sn-gateway-udp/logs/mqtt-sn-gateway-udp.log 2>&1 diff --git a/ec2-scripts/start.sh b/ec2-scripts/start.sh new file mode 100644 index 00000000..f7b93581 --- /dev/null +++ b/ec2-scripts/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +systemctl start mqtt-sn-gateway-udp.service diff --git a/ec2-scripts/stop.sh b/ec2-scripts/stop.sh new file mode 100644 index 00000000..b2d97a43 --- /dev/null +++ b/ec2-scripts/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +systemctl stop mqtt-sn-gateway-udp.service diff --git a/ec2-scripts/testing_build_triggers.sh b/ec2-scripts/testing_build_triggers.sh new file mode 100644 index 00000000..6c518bec --- /dev/null +++ b/ec2-scripts/testing_build_triggers.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Testing the CodeBuild commit message filter. +# First *without* the message regex... [BUILD] +# Trying again with the commit message regex +# WebHook test for CodePipeline after manual creation of JSON webhook. +# CodePipeline approval test via SNS diff --git a/ext/todo.txt b/ext/todo.txt new file mode 100644 index 00000000..eb29a8eb --- /dev/null +++ b/ext/todo.txt @@ -0,0 +1,3 @@ + +7. Will implementation +8. Forwarder packet implementation \ No newline at end of file diff --git a/images/mqttsn-arch.png b/images/mqttsn-arch.png new file mode 100644 index 00000000..3cc66299 Binary files /dev/null and b/images/mqttsn-arch.png differ diff --git a/images/waves-1400px.png b/images/waves-1400px.png new file mode 100644 index 00000000..fa3c2a1d Binary files /dev/null and b/images/waves-1400px.png differ diff --git a/images/waves-400px.png b/images/waves-400px.png new file mode 100644 index 00000000..8d2619a1 Binary files /dev/null and b/images/waves-400px.png differ diff --git a/mqtt-sn-client/README.md b/mqtt-sn-client/README.md new file mode 100644 index 00000000..6105e0cf --- /dev/null +++ b/mqtt-sn-client/README.md @@ -0,0 +1,72 @@ +# MQTT-SN Java Client +Full, dependency free java implementation of the MQTT-SN protocol specification for a client. +Uses the mqtt-sn-codecs for wire transport and comes equip with UDP network transport by default. +NOTE: As with all the modules in this project, the persistence, transport and wire traffic layer is entirely pluggable. + +## Quick start +Configure your details using the code below and run Example. + +```java +public class Example { + public static void main(String[] args) throws Exception { + + //-- use the client transport options, which will use random unallocated local ports + MqttsnUdpOptions udpOptions = new MqttsnClientUdpOptions(); + + //-- runtimes options can be used to tune the behaviour of the client + MqttsnOptions options = new MqttsnOptions(). + //-- specify the address of any static gateway nominating a context id for it + withNetworkAddressEntry("gatewayId", NetworkAddress.localhost(MqttsnUdpOptions.DEFAULT_LOCAL_PORT)). + //-- configure your clientId + withContextId("clientId1"). + //-- specify and predefined topic Ids that the gateway will know about + withPredefinedTopic("my/predefined/example/topic/1", 1); + + //-- using a default configuration for the controllers will just work out of the box, alternatively + //-- you can supply your own implementations to change underlying storage or business logic as is required + AbstractMqttsnRuntimeRegistry registry = MqttsnClientRuntimeRegistry.defaultConfiguration(options). + withTransport(new MqttsnUdpTransport(udpOptions)). + //-- select the codec you wish to use, support for SN 1.2 is standard or you can nominate your own + withCodec(MqttsnCodecs.MQTTSN_CODEC_VERSION_1_2); + + AtomicInteger receiveCounter = new AtomicInteger(); + CountDownLatch latch = new CountDownLatch(1); + + //-- the client is Closeable and so use a try with resource + try (MqttsnClient client = new MqttsnClient()) { + + //-- the client needs to be started using the configuration you constructed above + client.start(registry); + + //-- register any publish receive listeners you require + client.registerReceivedListener((IMqttsnContext context, String topic, int qos, byte[] data) -> { + receiveCounter.incrementAndGet(); + System.err.println(String.format("received message [%s] [%s]", + receiveCounter.get(), new String(data, MqttsnConstants.CHARSET))); + latch.countDown(); + }); + + //-- register any publish sent listeners you require + client.registerSentListener((IMqttsnContext context, UUID messageId, String topic, int qos, byte[] data) -> { + System.err.println(String.format("sent message [%s]", + new String(data, MqttsnConstants.CHARSET))); + }); + + //-- issue a connect command - the method will block until completion + client.connect(360, true); + + //-- issue a subscribe command - the method will block until completion + client.subscribe("my/example/topic/1", 2); + + //-- issue a publish command - the method will queue the message for sending and return immediately + client.publish("my/example/topic/1", 1, "hello world".getBytes()); + + //-- wait for the sent message to be looped back before closing + latch.await(30, TimeUnit.SECONDS); + + //-- issue a disconnect command - the method will block until completion + client.disconnect(); + } + } +} +``` diff --git a/mqtt-sn-client/dependency-reduced-pom.xml b/mqtt-sn-client/dependency-reduced-pom.xml new file mode 100644 index 00000000..c3917f92 --- /dev/null +++ b/mqtt-sn-client/dependency-reduced-pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + org.slj + mqtt-sn-client + 1.0.0 + + + + maven-compiler-plugin + + + maven-shade-plugin + 2.3 + + + package + + shade + + + + + org.slj.mqtt.sn.client.impl.cli.ClientInteractiveMain + + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + + junit + junit + 4.13 + test + + + hamcrest-core + org.hamcrest + + + + + + 3.8.1 + 1.8 + UTF-8 + 1.8 + 4.13 + + + diff --git a/mqtt-sn-client/pom.xml b/mqtt-sn-client/pom.xml new file mode 100644 index 00000000..f0af6801 --- /dev/null +++ b/mqtt-sn-client/pom.xml @@ -0,0 +1,103 @@ + + + + + 4.0.0 + + org.slj + mqtt-sn-client + 1.0.0 + + + UTF-8 + 4.13 + 3.8.1 + 1.8 + 1.8 + + + + + org.slj + mqtt-sn-codec + 1.0.0 + + + org.slj + mqtt-sn-core + 1.0.0 + + + junit + junit + ${junit.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + + + package + + shade + + + + + org.slj.mqtt.sn.client.impl.cli.ClientInteractiveMain + + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + \ No newline at end of file diff --git a/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/MqttsnClientConnectException.java b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/MqttsnClientConnectException.java new file mode 100644 index 00000000..0cc60ce0 --- /dev/null +++ b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/MqttsnClientConnectException.java @@ -0,0 +1,45 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.client; + +public class MqttsnClientConnectException extends Exception { + public MqttsnClientConnectException() { + } + + public MqttsnClientConnectException(String message) { + super(message); + } + + public MqttsnClientConnectException(String message, Throwable cause) { + super(message, cause); + } + + public MqttsnClientConnectException(Throwable cause) { + super(cause); + } +} diff --git a/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/MqttsnClient.java b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/MqttsnClient.java new file mode 100644 index 00000000..f69083dd --- /dev/null +++ b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/MqttsnClient.java @@ -0,0 +1,716 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.client.impl; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.client.MqttsnClientConnectException; +import org.slj.mqtt.sn.client.impl.examples.Example; +import org.slj.mqtt.sn.client.spi.IMqttsnClient; +import org.slj.mqtt.sn.impl.AbstractMqttsnRuntime; +import org.slj.mqtt.sn.model.*; +import org.slj.mqtt.sn.spi.*; +import org.slj.mqtt.sn.utils.MqttsnUtils; + +import java.io.IOException; +import java.util.Calendar; +import java.util.Date; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +/** + * Provides a blocking command implementation, with the ability to handle transparent reconnection + * during unsolicited disconnection events. + * + * Publishing occurs asynchronously and is managed by a FIFO queue. The size of the queue is determined + * by the configuration supplied. + * + * Connect, subscribe, unsubscribe and disconnect ( & sleep) are blocking calls which are considered successful on + * receipt of the correlated acknowledgement message. + * + * Management of the sleeping client state can either be supervised by the application, or by the client itself. During + * the sleep cycle, underlying resources (threads) are intelligently started and stopped. For example during sleep, the + * queue processing is closed down, and restarted during the Connected state. + * + * The client is {@link java.io.Closeable}. On close, a remote DISCONNECT operation is run (if required) and all encapsulated + * state (timers and threads) are stopped gracefully at best attempt. Once a client has been closed, it should be discarded and a new instance created + * should a new connection be required. + * + * For example use, please refer to {@link Example}. + */ +public class MqttsnClient extends AbstractMqttsnRuntime implements IMqttsnClient { + + private volatile MqttsnSessionState state; + private volatile int keepAlive; + private volatile boolean cleanSession; + + private volatile int errorRetryCounter = 0; + + private Thread managedConnectionThread = null; + private boolean managedConnection = false; + private volatile boolean autoReconnect = false; + + /** + * Construct a new client instance whose connection is NOT automatically managed. It will be up to the application + * to monitor and manage the connection lifecycle. + * + * If you wish to have the client supervise your connection (including active pings and unsolicited disconnect handling) then you + * should use the constructor which specifies managedConnected = true. + */ + public MqttsnClient(){ + this(false, false); + } + + /** + * Construct a new client instance specifying whether you want to client to automatically handle unsolicited DISCONNECT + * events. + * + * @param managedConnection - You can choose to use managed connections which will actively monitor your connection with the remote gateway, + * and issue PINGS where neccessary to keep your session alive + */ + public MqttsnClient(boolean managedConnection){ + this(managedConnection, true); + } + + /** + * Construct a new client instance specifying whether you want to client to automatically handle unsolicited DISCONNECT + * events. + * + * @param managedConnection - You can choose to use managed connections which will actively monitor your connection with the remote gateway, + * * and issue PINGS where neccessary to keep your session alive + * + * @param autoReconnect - When operating in managedConnection mode, should we attempt to silently reconnect if we detected a dropped + * connection + */ + public MqttsnClient(boolean managedConnection, boolean autoReconnect){ + this.managedConnection = managedConnection; + this.autoReconnect = autoReconnect; + registerConnectionListener(connectionListener); + } + + protected void resetErrorState(){ + errorRetryCounter = 0; + } + + @Override + protected void startupServices(IMqttsnRuntimeRegistry registry) throws MqttsnException { + + try { + Optional optionalContext = registry.getNetworkRegistry().first(); + if(!registry.getOptions().isEnableDiscovery() && + !optionalContext.isPresent()){ + throw new MqttsnRuntimeException("unable to launch non-discoverable client without configured gateway"); + } + } catch(NetworkRegistryException e){ + throw new MqttsnException("error using network registry", e); + } + + callStartup(registry.getMessageStateService()); + callStartup(registry.getMessageHandler()); + callStartup(registry.getMessageQueue()); + callStartup(registry.getMessageRegistry()); + callStartup(registry.getContextFactory()); + callStartup(registry.getSubscriptionRegistry()); + callStartup(registry.getTopicRegistry()); + callStartup(registry.getQueueProcessor()); + callStartup(registry.getTransport()); + } + + @Override + protected void stopServices(IMqttsnRuntimeRegistry registry) throws MqttsnException { + callShutdown(registry.getTransport()); + callShutdown(registry.getQueueProcessor()); + callShutdown(registry.getMessageStateService()); + callShutdown(registry.getMessageHandler()); + callShutdown(registry.getMessageQueue()); + callShutdown(registry.getMessageRegistry()); + callShutdown(registry.getContextFactory()); + callShutdown(registry.getSubscriptionRegistry()); + callShutdown(registry.getTopicRegistry()); + } + + @Override + public boolean isConnected() { + try { + IMqttsnSessionState state = checkSession(false); + return state.getClientState() == MqttsnClientState.CONNECTED; + } catch(MqttsnException e){ + return false; + } + } + + @Override + public boolean isAsleep() { + try { + IMqttsnSessionState state = checkSession(false); + return state.getClientState() == MqttsnClientState.ASLEEP; + } catch(MqttsnException e){ + return false; + } + } + + @Override + /** + * @see {@link IMqttsnClient#connect(int, boolean)} + */ + public void connect(int keepAlive, boolean cleanSession) throws MqttsnException, MqttsnClientConnectException{ + if(!MqttsnUtils.validUInt16(keepAlive)){ + throw new MqttsnExpectationFailedException("invalid keepAlive supplied"); + } + this.keepAlive = keepAlive; + this.cleanSession = cleanSession; + IMqttsnSessionState state = checkSession(false); + synchronized (this) { + //-- its assumed regardless of being already connected or not, if connect is called + //-- local state should be discarded + clearState(state.getContext(), cleanSession); + if (state.getClientState() != MqttsnClientState.CONNECTED) { + startProcessing(false); + try { + IMqttsnMessage message = registry.getMessageFactory().createConnect( + registry.getOptions().getContextId(), keepAlive, false, cleanSession); + MqttsnWaitToken token = registry.getMessageStateService().sendMessage(state.getContext(), message); + Optional response = + registry.getMessageStateService().waitForCompletion(state.getContext(), token); + stateChangeResponseCheck(state, token, response, MqttsnClientState.CONNECTED); + state.setKeepAlive(keepAlive); + startProcessing(true); + } catch(MqttsnExpectationFailedException e){ + //-- something was not correct with the CONNECT, shut it down again + logger.log(Level.WARNING, "error issuing CONNECT, disconnect"); + state.setClientState(MqttsnClientState.DISCONNECTED); + stopProcessing(); + throw new MqttsnClientConnectException(e); + } + } + } + } + + @Override + /** + * @see {@link IMqttsnClient#waitForCompletion(MqttsnWaitToken, int)} + */ + public Optional waitForCompletion(MqttsnWaitToken token, int customWaitTime) throws MqttsnExpectationFailedException { + synchronized (this){ + Optional response = registry.getMessageStateService().waitForCompletion( + state.getContext(), token, customWaitTime); + MqttsnUtils.responseCheck(token, response); + return response; + } + } + + @Override + /** + * @see {@link IMqttsnClient#publish(String, int, byte[])} + */ + public MqttsnWaitToken publish(String topicName, int QoS, byte[] data) throws MqttsnException, MqttsnQueueAcceptException{ + if(!MqttsnUtils.validQos(QoS)){ + throw new MqttsnExpectationFailedException("invalid QoS supplied"); + } + if(!MqttsnUtils.validTopicName(topicName)){ + throw new MqttsnExpectationFailedException("invalid topicName supplied"); + } + + if(QoS == -1){ + Integer alias = registry.getTopicRegistry().lookupPredefined(state.getContext(), topicName); + if(alias == null) + throw new MqttsnExpectationFailedException("can only publish to PREDEFINED topics at QoS -1"); + } + + IMqttsnSessionState state = checkSession(QoS >= 0); + UUID messageId = registry.getMessageRegistry().add(data, getMessageExpiry()); + MqttsnWaitToken token = registry.getMessageQueue().offer(state.getContext(), + new QueuedPublishMessage( + messageId, topicName, QoS)); + return token; + } + + @Override + /** + * @see {@link IMqttsnClient#subscribe(String, int)} + */ + public void subscribe(String topicName, int QoS) throws MqttsnException{ + if(!MqttsnUtils.validTopicName(topicName)){ + throw new MqttsnExpectationFailedException("invalid topicName supplied"); + } + if(!MqttsnUtils.validQos(QoS)){ + throw new MqttsnExpectationFailedException("invalid QoS supplied"); + } + IMqttsnSessionState state = checkSession(true); + + TopicInfo info = registry.getTopicRegistry().lookup(state.getContext(), topicName, true); + IMqttsnMessage message = null; + if(info == null || info.getType() == MqttsnConstants.TOPIC_TYPE.SHORT || + info.getType() == MqttsnConstants.TOPIC_TYPE.NORMAL){ + //-- the spec is ambiguous here; where a normalId has been obtained, it still requires use of + //-- topicName string + message = registry.getMessageFactory().createSubscribe(QoS, topicName); + } + else { + //-- only predefined should use the topicId as an uint16 + message = registry.getMessageFactory().createSubscribe(QoS, info.getType(), info.getTopicId()); + } + + synchronized (this){ + MqttsnWaitToken token = registry.getMessageStateService().sendMessage(state.getContext(), message); + Optional response = registry.getMessageStateService().waitForCompletion(state.getContext(), token); + MqttsnUtils.responseCheck(token, response); + } + } + + @Override + /** + * @see {@link IMqttsnClient#unsubscribe(String)} + */ + public void unsubscribe(String topicName) throws MqttsnException{ + if(!MqttsnUtils.validTopicName(topicName)){ + throw new MqttsnExpectationFailedException("invalid topicName supplied"); + } + IMqttsnSessionState state = checkSession(true); + + TopicInfo info = registry.getTopicRegistry().lookup(state.getContext(), topicName, true); + IMqttsnMessage message = null; + if(info == null || info.getType() == MqttsnConstants.TOPIC_TYPE.SHORT || + info.getType() == MqttsnConstants.TOPIC_TYPE.NORMAL){ + //-- the spec is ambiguous here; where a normalId has been obtained, it still requires use of + //-- topicName string + message = registry.getMessageFactory().createUnsubscribe(topicName); + } + else { + //-- only predefined should use the topicId as an uint16 + message = registry.getMessageFactory().createUnsubscribe(info.getType(), info.getTopicId()); + } + + synchronized (this){ + MqttsnWaitToken token = registry.getMessageStateService().sendMessage(state.getContext(), message); + Optional response = registry.getMessageStateService().waitForCompletion(state.getContext(), token); + MqttsnUtils.responseCheck(token, response); + } + } + + @Override + /** + * @see {@link IMqttsnClient#supervisedSleepWithWake(int, int, int, boolean)} + */ + public void supervisedSleepWithWake(int duration, int wakeAfterIntervalSeconds, int maxWaitTimeMillis, boolean connectOnFinish) + throws MqttsnException, MqttsnClientConnectException { + + if(!MqttsnUtils.validUInt16(duration)){ + throw new MqttsnExpectationFailedException("invalid duration supplied"); + } + + if(!MqttsnUtils.validUInt16(wakeAfterIntervalSeconds)){ + throw new MqttsnExpectationFailedException("invalid wakeAfterInterval supplied"); + } + + if(wakeAfterIntervalSeconds > duration) + throw new MqttsnExpectationFailedException("sleep duration must be greater than the wake after period"); + + long now = System.currentTimeMillis(); + long sleepUntil = now + (duration * 1000); + sleep(duration); + while(sleepUntil > (now = System.currentTimeMillis())){ + long timeLeft = sleepUntil - now; + long period = (int) Math.min(duration, timeLeft / 1000); + //-- sleep for the wake after period + try { + long wake = Math.min(wakeAfterIntervalSeconds, period); + if(wake > 0){ + logger.log(Level.INFO, String.format("waking after [%s] seconds", wake)); + Thread.sleep(wake * 1000); + wake(maxWaitTimeMillis); + } else { + break; + } + } catch(InterruptedException e){ + Thread.currentThread().interrupt(); + throw new MqttsnException(e); + } + } + + if(connectOnFinish){ + IMqttsnSessionState state = checkSession(false); + connect(state.getKeepAlive(), false); + } else { + startProcessing(false); + disconnect(); + } + } + + @Override + /** + * @see {@link IMqttsnClient#sleep(int)} + */ + public void sleep(int duration) throws MqttsnException{ + if(!MqttsnUtils.validUInt16(duration)){ + throw new MqttsnExpectationFailedException("invalid duration supplied"); + } + logger.log(Level.INFO, String.format("sleeping for [%s] seconds", duration)); + IMqttsnSessionState state = checkSession(true); + IMqttsnMessage message = registry.getMessageFactory().createDisconnect(duration); + synchronized (this){ + MqttsnWaitToken token = registry.getMessageStateService().sendMessage(state.getContext(), message); + Optional response = registry.getMessageStateService().waitForCompletion(state.getContext(), token); + stateChangeResponseCheck(state, token, response, MqttsnClientState.ASLEEP); + state.setKeepAlive(0); + clearState(state.getContext(),false); + stopProcessing(); + } + } + + @Override + /** + * @see {@link IMqttsnClient#wake()} + */ + public void wake() throws MqttsnException{ + wake(registry.getOptions().getMaxWait()); + } + + @Override + /** + * @see {@link IMqttsnClient#wake(int)} + */ + public void wake(int waitTime) throws MqttsnException{ + IMqttsnSessionState state = checkSession(false); + IMqttsnMessage message = registry.getMessageFactory().createPingreq(registry.getOptions().getContextId()); + synchronized (this){ + if(MqttsnUtils.in(state.getClientState(), + MqttsnClientState.ASLEEP)){ + startProcessing(false); + MqttsnWaitToken token = registry.getMessageStateService().sendMessage(state.getContext(), message); + state.setClientState(MqttsnClientState.AWAKE); + try { + Optional response = registry.getMessageStateService().waitForCompletion(state.getContext(), token, + waitTime); + stateChangeResponseCheck(state, token, response, MqttsnClientState.ASLEEP); + stopProcessing(); + } catch(MqttsnExpectationFailedException e){ + //-- this means NOTHING was received after my sleep - gateway may have gone, so disconnect and + //-- force CONNECT to be next operation + disconnect(false, false); + throw new MqttsnExpectationFailedException("gateway did not respond to AWAKE state; disconnected"); + } + } else { + throw new MqttsnExpectationFailedException("client cannot wake from a non-sleep state"); + } + } + } + + @Override + /** + * @see {@link IMqttsnClient#ping()} + */ + public void ping() throws MqttsnException{ + IMqttsnSessionState state = checkSession(true); + IMqttsnMessage message = registry.getMessageFactory().createPingreq(registry.getOptions().getContextId()); + synchronized (this){ + MqttsnWaitToken token = registry.getMessageStateService().sendMessage(state.getContext(), message); + registry.getMessageStateService().waitForCompletion(state.getContext(), token); + } + } + + @Override + /** + * @see {@link IMqttsnClient#disconnect()} + */ + public void disconnect() throws MqttsnException { + disconnect(true, true); + } + + private void disconnect(boolean sendRemoteDisconnect, boolean deepClean) throws MqttsnException { + try { + IMqttsnSessionState state = checkSession(false); + synchronized (this) { + if(state != null){ + try { + if (MqttsnUtils.in(state.getClientState(), + MqttsnClientState.CONNECTED, MqttsnClientState.ASLEEP, MqttsnClientState.AWAKE)) { + logger.log(Level.INFO, String.format("disconnecting client; deepClean ? [%s], sending remote disconnect ? [%s]", deepClean, sendRemoteDisconnect)); + clearState(state.getContext(), deepClean); + + if(sendRemoteDisconnect){ + IMqttsnMessage message = registry.getMessageFactory().createDisconnect(); + MqttsnWaitToken token = registry.getMessageStateService().sendMessage(state.getContext(), message); +// Optional response = registry.getMessageStateService().waitForCompletion(state.getContext(), token); +// stateChangeResponseCheck(state, token, response, MqttsnClientState.DISCONNECTED); + } + } + } finally { + state.setClientState(MqttsnClientState.DISCONNECTED); + } + } + } + } finally { + stopProcessing(); + } + } + + @Override + /** + * @see {@link IMqttsnClient#close()} + */ + public void close() throws IOException { + try { + disconnect(); + } catch(MqttsnException e){ + throw new IOException(e); + } finally { + try { + if(registry != null) + stop(); + } + catch(MqttsnException e){ + throw new IOException (e); + } finally { + if(managedConnectionThread != null){ + synchronized (managedConnectionThread){ + managedConnectionThread.notifyAll(); + } + } + } + } + } + + /** + * @see {@link IMqttsnClient#getClientId()} + */ + public String getClientId(){ + return registry.getOptions().getContextId(); + } + + private void stateChangeResponseCheck(IMqttsnSessionState sessionState, MqttsnWaitToken token, Optional response, MqttsnClientState newState) + throws MqttsnExpectationFailedException { + try { + MqttsnUtils.responseCheck(token, response); + if(response.isPresent() && + !response.get().isErrorMessage()){ + sessionState.setClientState(newState); + } + } catch(MqttsnExpectationFailedException e){ + logger.log(Level.SEVERE, "operation could not be completed, error in response"); + throw e; + } + } + + private MqttsnSessionState discoverGatewaySession() throws MqttsnException { + if(state == null){ + synchronized (this){ + if(state == null){ + try { + logger.log(Level.INFO, "discovering gateway..."); + Optional optionalMqttsnContext = + registry.getNetworkRegistry().waitForContext(registry.getOptions().getDiscoveryTime(), TimeUnit.SECONDS); + if(optionalMqttsnContext.isPresent()){ + INetworkContext gatewayContext = optionalMqttsnContext.get(); + state = new MqttsnSessionState(registry.getNetworkRegistry().getSessionContext(gatewayContext), MqttsnClientState.PENDING); + logger.log(Level.INFO, String.format("discovery located a gateway for use [%s]", gatewayContext)); + } else { + throw new MqttsnException("unable to discovery gateway within specified timeout"); + } + } catch(NetworkRegistryException | InterruptedException e){ + throw new MqttsnException("discovery was interrupted and no gateway was found", e); + } + } + } + } + return state; + } + + public int getPingDelta(){ + return (Math.max(keepAlive, 60) / registry.getOptions().getPingDivisor()); + } + + private void activateManagedConnection(){ + if(managedConnectionThread == null){ + managedConnectionThread = new Thread(() -> { + while(running){ + try { + synchronized (managedConnectionThread){ + long delta = errorRetryCounter > 0 ? registry.getOptions().getMaxErrorRetryTime() : getPingDelta() * 1000; + logger.log(Level.FINE, + String.format("managed connection monitor is running at time delta [%s], keepAlive [%s]...", delta, keepAlive)); + managedConnectionThread.wait(delta); + + if(running){ + synchronized (this){ //-- we could receive a unsolicited disconnect during passive reconnection | ping.. + IMqttsnSessionState state = checkSession(false); + if(state != null){ + if(state.getClientState() == MqttsnClientState.DISCONNECTED){ + if(autoReconnect){ + logger.log(Level.INFO, "client connection set to auto-reconnect..."); + connect(keepAlive, false); + resetErrorState(); + } + } + else if(state.getClientState() == MqttsnClientState.CONNECTED){ + if(keepAlive > 0){ //-- keepAlive 0 means alive forever, dont bother pinging + Long lastMessageSent = registry.getMessageStateService(). + getMessageLastSentToContext(state.getContext()); + if(lastMessageSent == null || System.currentTimeMillis() > + lastMessageSent + delta ){ + logger.log(Level.INFO, "managed connection issuing ping..."); + ping(); + resetErrorState(); + } + } + } + } + } + } + } + } catch(Exception e){ + try { + if(errorRetryCounter++ >= registry.getOptions().getMaxErrorRetries()){ + logger.log(Level.SEVERE, String.format("error [%s] on connection manager thread, DISCONNECTING", errorRetryCounter), e); + resetErrorState(); + disconnect(false, true); + } else { + registry.getMessageStateService().clearInflight(getSessionState().getContext()); + logger.log(Level.WARNING, String.format("error [%s] on connection manager thread, execute retransmission", errorRetryCounter), e); + } + } catch(Exception ex){ + logger.log(Level.WARNING, String.format("error handling retranmission [%s] on connection manager thread, execute retransmission", errorRetryCounter), e); + } + } + } + logger.log(Level.INFO, String.format("managed-connection closing down")); + }, "mqtt-sn-managed-connection"); + managedConnectionThread.setPriority(Thread.MIN_PRIORITY); + managedConnectionThread.setDaemon(true); + managedConnectionThread.start(); + } + } + + public void resetConnection(IMqttsnContext context, Throwable t, boolean attemptRestart) { + try { + logger.log(Level.WARNING, String.format("connection lost at transport layer [%s]", context), t); + disconnect(false, false); + //attempt to restart transport + callShutdown(registry.getTransport()); + if(attemptRestart){ + callStartup(registry.getTransport()); + if(managedConnectionThread != null){ + try { + synchronized (managedConnectionThread){ + managedConnectionThread.notify(); + } + } catch(Exception e){ + logger.log(Level.WARNING, String.format("error encountered when trying to recover from unsolicited disconnect [%s]", context, e)); + } + } + } + } catch(Exception e){ + logger.log(Level.WARNING, String.format("error encountered resetting connection [%s], current client state is [%s]", context, getSessionState()), e); + } + } + + private MqttsnSessionState checkSession(boolean validateConnected) throws MqttsnException { + MqttsnSessionState state = discoverGatewaySession(); + if(validateConnected && state.getClientState() != MqttsnClientState.CONNECTED) + throw new MqttsnRuntimeException("client not connected"); + return state; + } + + private final void stopProcessing() throws MqttsnException { + //-- ensure we stop message queue sending when we are not connected + registry.getMessageStateService().unscheduleFlush(state.getContext()); + callShutdown(registry.getMessageHandler()); + callShutdown(registry.getMessageStateService()); + } + + private final void startProcessing(boolean processQueue) throws MqttsnException { + + callStartup(registry.getTransport()); + callStartup(registry.getMessageStateService()); + callStartup(registry.getMessageHandler()); + if(processQueue){ + if(managedConnection){ + activateManagedConnection(); + } + } + } + + private void clearState(IMqttsnContext context, boolean deepClear) throws MqttsnException { + //-- unsolicited disconnect notify to the application + logger.log(Level.INFO, String.format("clearing state, deep clean ? [%s]", deepClear)); + registry.getMessageStateService().clearInflight(context); + registry.getTopicRegistry().clear(context, + registry.getOptions().isSleepClearsRegistrations()); + if(getSessionState() != null) state.setKeepAlive(0); + if(deepClear){ + registry.getSubscriptionRegistry().clear(context); + registry.getTopicRegistry().clear(context, true); +// registry.getMessageQueue().clear(context); + } + } + + private Date getMessageExpiry(){ + Calendar c = Calendar.getInstance(); + c.setTime(new Date()); + c.add(Calendar.HOUR, +1); + return c.getTime(); + } + + public IMqttsnSessionState getSessionState(){ + return state; + } + + protected IMqttsnConnectionStateListener connectionListener = + new IMqttsnConnectionStateListener() { + + @Override + public void notifyConnected(IMqttsnContext context) { + + } + + @Override + public void notifyRemoteDisconnect(IMqttsnContext context) { +// boolean shouldRecover = MqttsnUtils.in(state.getClientState(), +// MqttsnClientState.CONNECTED, MqttsnClientState.ASLEEP, MqttsnClientState.AWAKE); +// resetConnection(context, null, shouldRecover); + try { + disconnect(false, false); + } catch(Exception e){ + + } + } + + @Override + public void notifyActiveTimeout(IMqttsnContext context) { + + } + + @Override + public void notifyLocalDisconnect(IMqttsnContext context, Throwable t) { + + } + + @Override + public void notifyConnectionLost(IMqttsnContext context, Throwable t) { + resetConnection(context, t, true); + } + }; +} \ No newline at end of file diff --git a/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/MqttsnClientMessageHandler.java b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/MqttsnClientMessageHandler.java new file mode 100644 index 00000000..0d5c1436 --- /dev/null +++ b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/MqttsnClientMessageHandler.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.client.impl; + +import org.slj.mqtt.sn.client.spi.IMqttsnClientRuntimeRegistry; +import org.slj.mqtt.sn.codec.MqttsnCodecException; +import org.slj.mqtt.sn.impl.AbstractMqttsnMessageHandler; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.spi.IMqttsnMessage; +import org.slj.mqtt.sn.spi.MqttsnException; +import org.slj.mqtt.sn.wire.version1_2.payload.MqttsnAdvertise; +import org.slj.mqtt.sn.wire.version1_2.payload.MqttsnRegister; + +import java.util.logging.Level; + +public class MqttsnClientMessageHandler + extends AbstractMqttsnMessageHandler { + + @Override + protected IMqttsnMessage handleRegister(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException, MqttsnCodecException { + MqttsnRegister register = (MqttsnRegister) message; + if(register.getTopicId() > 0 && register.getTopicName() != null){ + registry.getTopicRegistry().register(context, register.getTopicName(), register.getTopicId()); + } + return super.handleRegister(context, message); + } + + @Override + protected void beforeHandle(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + } +} diff --git a/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/MqttsnClientRuntimeRegistry.java b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/MqttsnClientRuntimeRegistry.java new file mode 100644 index 00000000..da45c499 --- /dev/null +++ b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/MqttsnClientRuntimeRegistry.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.client.impl; + +import org.slj.mqtt.sn.client.spi.IMqttsnClientRuntimeRegistry; +import org.slj.mqtt.sn.impl.AbstractMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.impl.MqttsnContextFactory; +import org.slj.mqtt.sn.impl.MqttsnMessageQueueProcessor; +import org.slj.mqtt.sn.impl.ram.*; +import org.slj.mqtt.sn.model.MqttsnOptions; +import org.slj.mqtt.sn.net.NetworkAddressRegistry; + +public class MqttsnClientRuntimeRegistry extends AbstractMqttsnRuntimeRegistry implements IMqttsnClientRuntimeRegistry { + + public MqttsnClientRuntimeRegistry(MqttsnOptions options){ + super(options); + } + + public static MqttsnClientRuntimeRegistry defaultConfiguration(MqttsnOptions options){ + MqttsnClientRuntimeRegistry registry = (MqttsnClientRuntimeRegistry) new MqttsnClientRuntimeRegistry(options). + withContextFactory(new MqttsnContextFactory()). + withMessageHandler(new MqttsnClientMessageHandler()). + withMessageRegistry(new MqttsnInMemoryMessageRegistry()). + withNetworkAddressRegistry(new NetworkAddressRegistry(options.getMaxNetworkAddressEntries())). + withTopicRegistry(new MqttsnInMemoryTopicRegistry()). + withSubscriptionRegistry(new MqttsnInMemorySubscriptionRegistry()). + withMessageQueue(new MqttsnInMemoryMessageQueue()). + withQueueProcessor(new MqttsnMessageQueueProcessor(true)). + withMessageStateService(new MqttsnInMemoryMessageStateService(true)); + return registry; + } +} diff --git a/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/MqttsnClientUdpOptions.java b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/MqttsnClientUdpOptions.java new file mode 100644 index 00000000..7c5a632b --- /dev/null +++ b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/MqttsnClientUdpOptions.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.client.impl; + +import org.slj.mqtt.sn.net.MqttsnUdpOptions; + +public class MqttsnClientUdpOptions extends MqttsnUdpOptions { + { + //-- set all the client ports to wildcard 0 for localhost availability assignment + withPort(DEFAULT_LOCAL_CLIENT_PORT); + withSecurePort(DEFAULT_LOCAL_CLIENT_PORT); + withBroadcastPort(DEFAULT_LOCAL_CLIENT_PORT); + withBindBroadcastListener(true); + } +} diff --git a/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/cli/ClientInteractiveMain.java b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/cli/ClientInteractiveMain.java new file mode 100644 index 00000000..14f3e20c --- /dev/null +++ b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/cli/ClientInteractiveMain.java @@ -0,0 +1,44 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.client.impl.cli; + +import org.slj.mqtt.sn.net.MqttsnUdpOptions; +import org.slj.mqtt.sn.net.MqttsnUdpTransport; +import org.slj.mqtt.sn.spi.IMqttsnTransport; + +public class ClientInteractiveMain { + public static void main(String[] args) throws Exception { + MqttsnInteractiveClientLauncher.launch(new MqttsnInteractiveClient() { + protected IMqttsnTransport createTransport() { + MqttsnUdpOptions udpOptions = new MqttsnUdpOptions().withMtu(4096).withReceiveBuffer(4096). + withPort(MqttsnUdpOptions.DEFAULT_LOCAL_CLIENT_PORT); + return new MqttsnUdpTransport(udpOptions); + } + }); + } +} diff --git a/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/cli/MqttsnInteractiveClient.java b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/cli/MqttsnInteractiveClient.java new file mode 100644 index 00000000..0761f032 --- /dev/null +++ b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/cli/MqttsnInteractiveClient.java @@ -0,0 +1,494 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.client.impl.cli; + +import org.slj.mqtt.sn.cli.AbstractInteractiveCli; +import org.slj.mqtt.sn.client.MqttsnClientConnectException; +import org.slj.mqtt.sn.client.impl.MqttsnClient; +import org.slj.mqtt.sn.client.impl.MqttsnClientRuntimeRegistry; +import org.slj.mqtt.sn.codec.MqttsnCodecs; +import org.slj.mqtt.sn.impl.AbstractMqttsnRuntime; +import org.slj.mqtt.sn.impl.AbstractMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.impl.ram.MqttsnInMemoryTopicRegistry; +import org.slj.mqtt.sn.model.MqttsnClientState; +import org.slj.mqtt.sn.model.MqttsnOptions; +import org.slj.mqtt.sn.model.MqttsnQueueAcceptException; +import org.slj.mqtt.sn.model.Subscription; +import org.slj.mqtt.sn.net.NetworkAddress; +import org.slj.mqtt.sn.spi.IMqttsnTransport; +import org.slj.mqtt.sn.spi.MqttsnException; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +public abstract class MqttsnInteractiveClient extends AbstractInteractiveCli { + + static final String TEST_TOPIC = "test/topic"; + static final int TEST_PAUSE = 500; + static final int[] ALLOWED_QOS = new int[]{-1,0,1,2}; + + enum COMMANDS { + LOOP("Create messages in a loop", new String[]{"int count", "String* topicName", "String* data", "int QoS"}), + STATS("View statistics for runtime", new String[0]), + RESET("Reset the stats and the local queue", new String[0]), + CONNECT("Connect to the gateway and establish a new session", new String[]{"boolean cleanSession", "int16 keepAlive"}), + SUBSCRIBE("Subscribe to a topic", new String[]{"String* topicName", "int QoS"}), + DISCONNECT("Disconnect from the gateway session", new String[0]), + SLEEP("Send the remote session to sleep", new String[]{"int16 duration"}), + WAKE("Wake from a sleep to check for messages", new String[0]), + PUBLISH("Publish a new message", new String[]{"String* topicName", "String* data", "int QoS"}), + UNSUBSCRIBE("Unsubscribe an existing topic subscription", new String[]{"String* topicName"}), + STATUS("Obtain the status of the client", new String[0]), + TEST("Execute an built in test suite", new String[0]), + PREDEFINE("Add a predefined topic alias", new String[]{"String* topicName", "int16 topicAlias"}), + HELP("List this message", new String[0]), + QUIT("Quit the application", new String[0]), + EXIT("Quit the application", new String[0], true), + BYE("Quit the application", new String[0], true); + + private String description; + private String[] arguments; + private boolean hidden = false; + + COMMANDS(String description, String[] arguments, boolean hidden){ + this(description, arguments); + this.hidden = hidden; + } + + COMMANDS(String description, String[] arguments){ + this.description = description; + this.arguments = arguments; + } + + public boolean isHidden() { + return hidden; + } + + public String[] getArguments() { + return arguments; + } + + public String getDescription(){ + return description; + } + } + + protected boolean processCommand(String command) throws Exception { + COMMANDS c = COMMANDS.valueOf(command.toUpperCase()); + processCommand(c); + if(c == COMMANDS.QUIT || c == COMMANDS.BYE || c == COMMANDS.EXIT){ + return false; + } + return true; + } + + protected void processCommand(COMMANDS command) throws IOException { + try { + switch (command){ + case HELP: + for(COMMANDS c : COMMANDS.values()){ + if(c.isHidden()) continue; + StringBuilder sb = new StringBuilder(); + for(String a : c.getArguments()){ + if(sb.length() > 0){ + sb.append(", "); + } + sb.append(a); + } + + output.println("\t" + c.name()); + output.println("\t\t" + c.getDescription()); + } + break; + case CONNECT: + connect( + captureMandatoryBoolean(input, output, "Would you like a clean session?"), + captureMandatoryInt(input, output, "How long would you like your keepAlive to be (in seconds)?", null)); + break; + case SUBSCRIBE: + subscribe( + captureMandatoryString(input, output, "Which topic would you like to subscribe to?"), + captureMandatoryInt(input, output, "At which QoS would you like to subscribe (0,1,2)?", ALLOWED_QOS)); + break; + case UNSUBSCRIBE: + unsubscribe( + captureMandatoryString(input, output, "Which topic would you like to unsubscribe from?")); + break; + case LOOP: + loop( + captureMandatoryInt(input, output, "How many messages would you like to send?", null), + captureMandatoryString(input, output, "Which topic would you like to publish to?"), + captureMandatoryInt(input, output, "At which QoS would you like to publish (-1,0,1,2)?", ALLOWED_QOS)); + break; + case PUBLISH: + publish( + captureMandatoryString(input, output, "Which topic would you like to publish to?"), + captureMandatoryString(input, output, "What is the message you would like to publish?"), + captureMandatoryInt(input, output, "At which QoS would you like to publish (-1,0,1,2)?", ALLOWED_QOS)); + break; + case SLEEP: + sleep(captureMandatoryInt(input, output, "How long would you like to sleep for (in seconds)?", null)); + break; + case WAKE: + wake(); + break; + case STATS: + stats(); + break; + case RESET: + resetMetrics(); + break; + case STATUS: + status(); + break; + case TEST: + test(); + break; + case PREDEFINE: + predefine( + captureMandatoryString(input, output, "What is the topic you would like to predefine?"), + captureMandatoryInt(input, output, "What is the alias for the topic?", null)); + break; + case EXIT: + case BYE: + case QUIT: + quit(); + case DISCONNECT: + disconnect(); + break; + } + } catch(Exception e){ + error( "An error occurred running your command.", e); + } + } + + @Override + protected String getCLIName(){ + return "org slj Mqtt-sn interactive client"; + } + + protected void connect(boolean cleanSession, int keepAlive) + throws IOException, MqttsnException { + MqttsnClient client = (MqttsnClient) getRuntime(); + if(client != null && !client.isConnected()){ + try { + resetMetrics(); + client.connect(keepAlive, cleanSession); + message("DONE - connect issued successfully, client is connected"); + } catch(MqttsnClientConnectException e){ + error("Client Reporting Connection Error", e); + } + } else { + message("Client is already connected"); + } + } + + protected void loop(int count, String topicPath, int qos) + throws IOException, MqttsnException { + for (int i = 0; i < count; i++){ + publish(topicPath, "message " + i, qos); + try { + Thread.sleep(1); + } catch(Exception e){} + } + } + + protected void publish(String topicPath, String data, int qos) + throws IOException, MqttsnException { + MqttsnClient client = (MqttsnClient) getRuntime(); + if(client != null && (client.isConnected() || qos == -1)){ + try { + client.publish(topicPath, qos, data.getBytes(StandardCharsets.UTF_8)); + if(!client.isConnected()){ + boolean stopAfterUse = false; + try { + if(!getRuntimeRegistry().getQueueProcessor().running()){ + getRuntimeRegistry().getQueueProcessor().start(getRuntimeRegistry()); + stopAfterUse = true; + } + getRuntimeRegistry().getMessageStateService().scheduleFlush( + client.getSessionState().getContext()); + try { + Thread.sleep(1000); + } catch(Exception e){ + } + } finally{ + if(stopAfterUse) getRuntimeRegistry().getQueueProcessor().stop(); + } + message("DONE - message sent and flushed on DISCONNECTED session"); + } + else { + message("DONE - message queued for sending"); + } + } catch(MqttsnQueueAcceptException e){ + error("Client Reporting Queue Accept Error", e); + } + } else { + message("Client must first be connected before issuing this command"); + } + } + + protected void subscribe(String topicPath, int qos) + throws IOException, MqttsnException { + MqttsnClient client = (MqttsnClient) getRuntime(); + if(client != null && client.isConnected()){ + client.subscribe(topicPath, qos); + message("DONE - subscribe issued successfully"); + } else { + message("Client must first be connected before issuing this command"); + } + } + + protected void unsubscribe(String topicPath) + throws IOException, MqttsnException { + MqttsnClient client = (MqttsnClient) getRuntime(); + if(client != null && client.isConnected()){ + client.unsubscribe(topicPath); + message("DONE - unsubscribe issued successfully"); + } else { + message("Client must first be connected before issuing this command"); + } + } + + protected void sleep(int duration) + throws IOException, MqttsnException { + MqttsnClient client = (MqttsnClient) getRuntime(); + if(client != null && client.isConnected()){ + client.sleep(duration); + message(String.format("DONE - client is sleeping for %s seconds", duration)); + } else { + message("Client must first be connected before issuing this command"); + } + } + + protected void wake() + throws IOException, MqttsnException { + MqttsnClient client = (MqttsnClient) getRuntime(); + if(client != null && client.isAsleep()){ + client.wake(60000); + message(String.format("DONE - client received all messages and is back sleeping")); + } else { + message("Client must first be asleep before issuing this command"); + } + } + + protected void quit() + throws MqttsnException, IOException { + MqttsnClient client = (MqttsnClient) getRuntime(); + if(client != null){ + stop(); + message("Client stopped - bye :-)"); + } + } + + public void stop() + throws MqttsnException, IOException { + MqttsnClient client = (MqttsnClient) getRuntime(); + if(client != null){ + client.close(); + } + } + + protected void disconnect() + throws IOException, MqttsnException { + MqttsnClient client = (MqttsnClient) getRuntime(); + if(client != null){ + client.disconnect(); + message("DONE - client is disconnected"); + } + } + + @Override + public void resetMetrics() throws IOException { + super.resetMetrics(); + if(runtime != null && runtimeRegistry != null){ + try { + MqttsnClient client = (MqttsnClient) getRuntime(); + runtimeRegistry.getMessageQueue().clear(client.getSessionState().getContext()); + } catch(Exception e){ + error("error clearing queue;", e); + } + } + } + + protected void status() + throws IOException, MqttsnException { + MqttsnClient client = (MqttsnClient) getRuntime(); + + message("Client Id: " + clientId); + if(client != null){ + if(runtime != null) { + message( "Receive Publish Count: " + receiveCount + " ("+receivedPublishBytesCount+" bytes)"); + message( "Sent Publish Count: " + sentCount + " ("+publishedBytesCount+" bytes)"); + if(client.getSessionState() != null){ + message( "Client Started: " + client.getSessionState().getSessionStarted()); + message( "Client Session State: " + getConnectionString(client.getSessionState().getClientState())); + message( "Keep Alive: " + client.getSessionState().getKeepAlive()); + message( "Ping Interval: " + client.getPingDelta() + " seconds"); + + Long lastSent = getRuntimeRegistry().getMessageStateService().getMessageLastSentToContext(client.getSessionState().getContext()); + if(lastSent != null){ + message( "Last Message Sent: " + new Date(lastSent)); + } + + Long lastReceived = getRuntimeRegistry().getMessageStateService().getMessageLastReceivedFromContext(client.getSessionState().getContext()); + if(lastReceived != null){ + message( "Last Message Received: " + new Date(lastReceived)); + } + + if (getRuntimeRegistry().getMessageQueue() != null) { + message( "Publish Queue Size: " + getRuntimeRegistry().getMessageQueue().size(client.getSessionState().getContext())); + } + } + if (getOptions() != null) { + Map pTopics = getOptions().getPredefinedTopics(); + if(pTopics != null){ + message( "Predefined Topic Count: " + pTopics.size()); + Iterator itr = pTopics.keySet().iterator(); + while(itr.hasNext()){ + String topic = itr.next(); + message( "\t" + topic + " = " + pTopics.get(topic)); + } + } + } + + if(client.getSessionState() != null){ + Set subs = getRuntimeRegistry().getSubscriptionRegistry().readSubscriptions(client.getSessionState().getContext()); + Iterator itr = subs.iterator(); + message("Subscription(s): "); + synchronized (subs) { + while (itr.hasNext()) { + Subscription s = itr.next(); + message("\t" + s.getTopicPath() + " -> " + s.getQoS()); + } + } + + if(getRuntimeRegistry().getTopicRegistry() instanceof MqttsnInMemoryTopicRegistry){ + Set s = ((MqttsnInMemoryTopicRegistry)getRuntimeRegistry().getTopicRegistry() ).getAll(client.getSessionState().getContext()); + if(s != null){ + message( "Registered Topic Count: " + s.size()); + for(MqttsnInMemoryTopicRegistry.ConfirmableTopicRegistration t : s){ + message( "\t" + t.getTopicPath() + " = " + t.getAliasId() + " ? " + t.isConfirmed()); + } + } + } + } + + if (getRuntimeRegistry().getQueueProcessor() != null) { + message( "Queue Processor: " + (getRuntimeRegistry().getQueueProcessor().running() ? "Running" : "Stopped")); + } + } + } else { + message( "Client Status: Awaiting Connection.."); + } + } + + protected void test() + throws IOException { + MqttsnClient client = (MqttsnClient) getRuntime(); + if(client != null && !client.isConnected()){ + try { + resetMetrics(); + Thread.sleep(TEST_PAUSE); + connect(true, 60); + Thread.sleep(TEST_PAUSE); + subscribe(TEST_TOPIC, 2); + Thread.sleep(TEST_PAUSE); + publish(TEST_TOPIC, "test qos 0", 0); + publish(TEST_TOPIC, "test qos 1", 1); + publish(TEST_TOPIC, "test qos 2", 2); + Thread.sleep(20000); + disconnect(); + message("Tests have finished"); + } catch(Exception e){ + error("Client Reporting Queue Accept Error", e); + } + } else { + message("Client must first be disconnected before running tests, please issue DISCONNECT command"); + } + } + + @Override + protected MqttsnOptions createOptions() throws UnknownHostException { + return new MqttsnOptions(). + withNetworkAddressEntry("remote-gateway", + NetworkAddress.from(port, hostName)). + withContextId(clientId). + withMaxMessagesInQueue(100000). + withMinFlushTime(0). + withMaxProtocolMessageSize(4096). + withSleepClearsRegistrations(false); + } + + @Override + protected AbstractMqttsnRuntimeRegistry createRuntimeRegistry(MqttsnOptions options, IMqttsnTransport transport) { + AbstractMqttsnRuntimeRegistry registry = MqttsnClientRuntimeRegistry.defaultConfiguration(options). + withTransport(transport). + withCodec(MqttsnCodecs.MQTTSN_CODEC_VERSION_1_2); + return registry; + } + + @Override + protected AbstractMqttsnRuntime createRuntime(AbstractMqttsnRuntimeRegistry registry, MqttsnOptions options) { + MqttsnClient runtime = new MqttsnClient(true, false); + return runtime; + } + + @Override + public void start() throws Exception { + super.start(); + getRuntime().start(getRuntimeRegistry()); + } + + @Override + protected String getPropertyFileName() { + return "client.properties"; + } + + protected String getConnectionString(MqttsnClientState state){ + if(state == null) return "NOINIT"; + switch (state){ + case AWAKE: + return cli_green() + state; + case CONNECTED: + return cli_green() + state; + case ASLEEP: + return cli_blue() + state; + case DISCONNECTED: + return cli_red() + state; + case PENDING: + default: + return cli_reset() + state; + } + } +} diff --git a/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/cli/MqttsnInteractiveClientLauncher.java b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/cli/MqttsnInteractiveClientLauncher.java new file mode 100644 index 00000000..f82273b7 --- /dev/null +++ b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/cli/MqttsnInteractiveClientLauncher.java @@ -0,0 +1,53 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.client.impl.cli; + +import java.io.PrintStream; +import java.util.Scanner; +import java.util.logging.LogManager; + +public class MqttsnInteractiveClientLauncher { + public static void launch(MqttsnInteractiveClient interactiveClient) throws Exception { + if(!Boolean.getBoolean("debug")) LogManager.getLogManager().reset(); + Scanner input = new Scanner(System.in); + PrintStream output = System.out; + try { + interactiveClient.init(input, output); + interactiveClient.welcome(); + interactiveClient.configureWithHistory(); + interactiveClient.start(); + interactiveClient.command(); + interactiveClient.exit(); + } catch(Exception e){ + System.err.println("A fatal error was encountered: " + e.getMessage()); + } finally { + input.close(); + interactiveClient.stop(); + } + } +} diff --git a/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/examples/Example.java b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/examples/Example.java new file mode 100644 index 00000000..457a06d0 --- /dev/null +++ b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/impl/examples/Example.java @@ -0,0 +1,109 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.client.impl.examples; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.client.impl.MqttsnClient; +import org.slj.mqtt.sn.client.impl.MqttsnClientRuntimeRegistry; +import org.slj.mqtt.sn.client.impl.MqttsnClientUdpOptions; +import org.slj.mqtt.sn.codec.MqttsnCodecs; +import org.slj.mqtt.sn.impl.AbstractMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.MqttsnOptions; +import org.slj.mqtt.sn.net.MqttsnUdpOptions; +import org.slj.mqtt.sn.net.MqttsnUdpTransport; +import org.slj.mqtt.sn.net.NetworkAddress; +import org.slj.mqtt.sn.utils.MqttsnUtils; + +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class Example { + public static void main(String[] args) throws Exception { + + //-- use the client transport options, which will use random unallocated local ports + MqttsnUdpOptions udpOptions = new MqttsnClientUdpOptions(); + + //-- runtimes options can be used to tune the behaviour of the client + MqttsnOptions options = new MqttsnOptions(). + //-- specify the address of any static gateway nominating a context id for it + withNetworkAddressEntry("gatewayId", NetworkAddress.localhost(MqttsnUdpOptions.DEFAULT_LOCAL_PORT)). + //-- configure your clientId + withContextId("clientId1"). + //-- specify and predefined topic Ids that the gateway will know about + withPredefinedTopic("my/predefined/example/topic/1", 1); + + //-- using a default configuration for the controllers will just work out of the box, alternatively + //-- you can supply your own implementations to change underlying storage or business logic as is required + AbstractMqttsnRuntimeRegistry registry = MqttsnClientRuntimeRegistry.defaultConfiguration(options). + withTransport(new MqttsnUdpTransport(udpOptions)). + //-- select the codec you wish to use, support for SN 1.2 is standard or you can nominate your own + withCodec(MqttsnCodecs.MQTTSN_CODEC_VERSION_1_2); + + AtomicInteger receiveCounter = new AtomicInteger(); + CountDownLatch latch = new CountDownLatch(1); + + //-- the client is Closeable and so use a try with resource + try (MqttsnClient client = new MqttsnClient()) { + + //-- the client needs to be started using the configuration you constructed above + client.start(registry); + + //-- register any publish receive listeners you require + client.registerReceivedListener((IMqttsnContext context, String topic, int qos, byte[] data) -> { + receiveCounter.incrementAndGet(); + System.err.println(String.format("received message [%s] [%s]", + receiveCounter.get(), new String(data, MqttsnConstants.CHARSET))); + latch.countDown(); + }); + + //-- register any publish sent listeners you require + client.registerSentListener((IMqttsnContext context, UUID messageId, String topic, int qos, byte[] data) -> { + System.err.println(String.format("sent message [%s]", + new String(data, MqttsnConstants.CHARSET))); + }); + + //-- issue a connect command - the method will block until completion + client.connect(360, true); + + //-- issue a subscribe command - the method will block until completion + client.subscribe("my/example/topic/1", 2); + + //-- issue a publish command - the method will queue the message for sending and return immediately + client.publish("my/example/topic/1", 1, "hello world".getBytes()); + + //-- wait for the sent message to be looped back before closing + latch.await(30, TimeUnit.SECONDS); + + //-- issue a disconnect command - the method will block until completion + client.disconnect(); + } + } +} diff --git a/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/spi/IMqttsnClient.java b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/spi/IMqttsnClient.java new file mode 100644 index 00000000..a59151a5 --- /dev/null +++ b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/spi/IMqttsnClient.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.client.spi; + +import org.slj.mqtt.sn.client.MqttsnClientConnectException; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.MqttsnQueueAcceptException; +import org.slj.mqtt.sn.model.MqttsnWaitToken; +import org.slj.mqtt.sn.spi.*; +import org.slj.mqtt.sn.utils.MqttsnUtils; + +import java.io.Closeable; +import java.util.Optional; + +/** + * An SN client allows you to talk to a DISCOVERED or PRECONFIGURED Sensor Network gateway. + */ +public interface IMqttsnClient extends Closeable { + + /** + * Is the client in the CONNECTED state + */ + boolean isConnected(); + + /** + * Is the client in the ASLEEP state + */ + boolean isAsleep(); + + /** + * A blocking call to issue a CONNECT packet. On return your client will be considered in ACTIVE mode unless an exception + * is thrown. + * + * @param keepAlive - Time in seconds to keep the session alive before the gateway times you out + * @param cleanSession - Whether tidy up any existing session state on the gateway; including message queues, subscriptions and registrations + * @throws MqttsnException - There was an internal error + * @throws MqttsnClientConnectException - The connect handshake could not be completed + */ + void connect(int keepAlive, boolean cleanSession) throws MqttsnException, MqttsnClientConnectException; + + /** + * Add a new message onto the queue to send to the gateway at some point in the future. + * The queue is processed in FIFO order in the background on a queue processing thread. + * You can be notified of successful completion by registering a messageSent listener + * onto the client. + * + * @param topicName - The path to which you wish to send the data + * @param QoS - Quality of Service of the method, one of -1, 0 , 1, 2 + * @param data - The data you wish to send + * @return token - a sending token that you can use to block on until the message has been sent + * @throws MqttsnException - There was an internal error + * @throws MqttsnQueueAcceptException - The queue could not accept the message, most likely full + */ + MqttsnWaitToken publish(String topicName, int QoS, byte[] data) throws MqttsnException, MqttsnQueueAcceptException; + + + /** + * @see {@link IMqttsnMessageStateService#waitForCompletion} + */ + Optional waitForCompletion(MqttsnWaitToken token, int customWaitTime) throws MqttsnExpectationFailedException; + + + /** + * Subscribe the topic using the most appropriate topic scheme. This will automatically select the use of a PREDEFINED or SHORT + * according to your configuration. If no PREDEFINED or SHORT topic is available, a new NORMAL registration will be placed in your + * topic registry transparently. + * + * @param topicName - The path to subscribe to + * @param QoS - The quality of service of the subscription + * @throws MqttsnException - An error occurred + */ + void subscribe(String topicName, int QoS) throws MqttsnException; + + /** + * Unsubscribe the topic using the most appropriate topic scheme. This will automatically select th use of a PREDEFINED or SHORT + * according to your configuration. If no PREDEFINED or SHORT topic is available, a new registration will be places in your + * topic registry transparently for receiving the messages. + * + * @param topicName - The path to unsubscribe to + * @throws MqttsnException - An error occurred + */ + void unsubscribe(String topicName) throws MqttsnException; + + /** + * A long blocking call which will put your client into the SLEEP state for the {duration} specified in SECONDS, automatically + * waking every {wakeAfterInterval} period of time in SECONDS to check for messages. NOTE: messages queued to be sent while + * the device is SLEEPing will not be processed until the device is back in the ACTIVE status. + * + * @param duration - time in seconds for the sleep period to last + * @param wakeAfterIntervalSeconds - time in seconds that the device will wake up to check for messages, before going back to sleep + * @param maxWaitTimeMillis - time in millis during WAKING that the device will wait for a PINGRESP response from the gateway before erroring + * @param connectOnFinish - when the DURATION period has elapsed, should the device transition into the ACTIVE mode by issuing a soft CONNECT or alternatively, DISCONNECT + * @throws MqttsnException - An error occurred + */ + void supervisedSleepWithWake(int duration, int wakeAfterIntervalSeconds, int maxWaitTimeMillis, boolean connectOnFinish) throws MqttsnException, MqttsnClientConnectException; + + /** + * Put the device into the SLEEP mode for the duration in seconds. NOTE: this is a non-supervized sleep, which means the application + * is responsible for issuing PINGREQ and CONNECTS from this mode + * @param duration - Time in seconds to put the device to sleep. + * @throws MqttsnException - An error occurred + */ + void sleep(int duration) throws MqttsnException; + + /** + * Unsupervised Wake the device up by issuing a PINGREQ from SLEEP state. The maxWait time will be taken from the core client configuration + * supplied during setup. + * @throws MqttsnException - An error occurred + */ + void wake() throws MqttsnException; + + /** + * Unsupervised Wake the device up by issuing a PINGREQ from SLEEP state. + * @param waitTime - Time in MILLISECONDS to wait for a PINGRESP after the AWAKE period + * @throws MqttsnException - An error occurred + */ + void wake(int waitTime) throws MqttsnException; + + /** + * Issue a PINGREQ during CONNECTED mode. This operation will not affect the state of the runtime, with the exception of + * updating the last message sent time timestamp + * @throws MqttsnException - An error occurred + */ + void ping() throws MqttsnException; + + /** + * DISCONNECT from the gateway. Closes down any local queues and active processing + */ + void disconnect() throws MqttsnException; + + /** + * Registers a new Publish listener which will be notified when a PUBLISH message is successfully committed to the gateway + * @param listener - The instance of the listener to notify + */ + void registerSentListener(IMqttsnPublishSentListener listener); + + /** + * Registers a new Publish listener which will be notified when a PUBLISH message is successfully RECEIVED committed from the gateway + * @param listener - The instance of the listener to notify + */ + void registerReceivedListener(IMqttsnPublishReceivedListener listener); + + /** + * Return the clientId associated with this instance. The clientId is passed to the client from the configuration (contextId). + * @return - Return the clientId associated with this instance + */ + String getClientId(); + +} \ No newline at end of file diff --git a/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/spi/IMqttsnClientRuntimeRegistry.java b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/spi/IMqttsnClientRuntimeRegistry.java new file mode 100644 index 00000000..ce6dad64 --- /dev/null +++ b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/spi/IMqttsnClientRuntimeRegistry.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.client.spi; + +import org.slj.mqtt.sn.spi.IMqttsnRuntimeRegistry; + +public interface IMqttsnClientRuntimeRegistry extends IMqttsnRuntimeRegistry { + +} diff --git a/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/spi/IMqttsnClientService.java b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/spi/IMqttsnClientService.java new file mode 100644 index 00000000..709dd5f3 --- /dev/null +++ b/mqtt-sn-client/src/main/java/org/slj/mqtt/sn/client/spi/IMqttsnClientService.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.client.spi; + +import org.slj.mqtt.sn.spi.IMqttsnService; +import org.slj.mqtt.sn.spi.MqttsnException; + +public interface IMqttsnClientService extends IMqttsnService { + + void start(IMqttsnClientRuntimeRegistry runtime) throws MqttsnException; + + void stop() throws MqttsnException; +} \ No newline at end of file diff --git a/mqtt-sn-client/src/test/java/org/slj/mqtt/sn/client/test/ClientConnectionTest.java b/mqtt-sn-client/src/test/java/org/slj/mqtt/sn/client/test/ClientConnectionTest.java new file mode 100644 index 00000000..4803f43b --- /dev/null +++ b/mqtt-sn-client/src/test/java/org/slj/mqtt/sn/client/test/ClientConnectionTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.client.test; + +import org.junit.Assert; +import org.junit.Test; +import org.slj.mqtt.sn.client.MqttsnClientConnectException; +import org.slj.mqtt.sn.client.impl.MqttsnClient; +import org.slj.mqtt.sn.client.impl.MqttsnClientRuntimeRegistry; +import org.slj.mqtt.sn.client.impl.MqttsnClientUdpOptions; +import org.slj.mqtt.sn.codec.MqttsnCodecs; +import org.slj.mqtt.sn.model.IMqttsnSessionState; +import org.slj.mqtt.sn.model.MqttsnClientState; +import org.slj.mqtt.sn.model.MqttsnOptions; +import org.slj.mqtt.sn.net.MqttsnUdpOptions; +import org.slj.mqtt.sn.net.MqttsnUdpTransport; +import org.slj.mqtt.sn.net.NetworkAddress; +import org.slj.mqtt.sn.spi.MqttsnException; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +public class ClientConnectionTest { + + static final String TOPIC = "t/%s"; + static final int CONNECT_TIMEOUT = 1000; + static final int MUTLI_CLIENT_LATCH_TIMEOUT = 240; + static final byte[] PAYLOAD = new byte[]{0x01,0x02,0x03}; + + protected MqttsnClientRuntimeRegistry createClientRuntimeRegistry(String clientId){ + MqttsnUdpOptions udpOptions = new MqttsnClientUdpOptions(); + MqttsnOptions options = new MqttsnOptions(). + withNetworkAddressEntry("gatewayId", + NetworkAddress.localhost(MqttsnUdpOptions.DEFAULT_LOCAL_PORT)). + withContextId(clientId + "-" + ThreadLocalRandom.current().nextLong()). + withMaxWait(60000). + withPredefinedTopic("my/example/topic/1", 1); + + return (MqttsnClientRuntimeRegistry) MqttsnClientRuntimeRegistry.defaultConfiguration(options). + withTransport(new MqttsnUdpTransport(udpOptions)). + withCodec(MqttsnCodecs.MQTTSN_CODEC_VERSION_1_2); + } + + @Test + public void testClientConnection() throws IOException, MqttsnException, MqttsnClientConnectException { + try (MqttsnClient client = new MqttsnClient()) { + client.start(createClientRuntimeRegistry("testClientId")); + client.connect(CONNECT_TIMEOUT, true); + assertClientSessionState(client, MqttsnClientState.CONNECTED); + } + } + + @Test + public void testClientDoubleConnection() throws IOException, MqttsnException, MqttsnClientConnectException { + try (MqttsnClient client = new MqttsnClient()) { + client.start(createClientRuntimeRegistry("testClientId")); + client.connect(CONNECT_TIMEOUT, true); + assertClientSessionState(client, MqttsnClientState.CONNECTED); + client.connect(CONNECT_TIMEOUT, true); + assertClientSessionState(client, MqttsnClientState.CONNECTED); + } + } + + @Test + public void testClientDisconnectedAfterClose() throws IOException, MqttsnException, MqttsnClientConnectException { + try (MqttsnClient client = new MqttsnClient()) { + client.start(createClientRuntimeRegistry("testClientId")); + client.connect(CONNECT_TIMEOUT, true); + assertClientSessionState(client, MqttsnClientState.CONNECTED); + client.disconnect(); + assertClientSessionState(client, MqttsnClientState.DISCONNECTED); + } + } + + @Test + public void testClientSleep() throws IOException, MqttsnException, MqttsnClientConnectException { + try (MqttsnClient client = new MqttsnClient()) { + client.start(createClientRuntimeRegistry("testClientId")); + client.connect(CONNECT_TIMEOUT, true); + assertClientSessionState(client, MqttsnClientState.CONNECTED); + client.sleep(CONNECT_TIMEOUT); + assertClientSessionState(client, MqttsnClientState.ASLEEP); + } + } + + @Test + public void testClientWake() throws IOException, MqttsnException, MqttsnClientConnectException { + try (MqttsnClient client = new MqttsnClient()) { + client.start(createClientRuntimeRegistry("testClientId")); + client.connect(CONNECT_TIMEOUT, true); + assertClientSessionState(client, MqttsnClientState.CONNECTED); + client.sleep(CONNECT_TIMEOUT); + assertClientSessionState(client, MqttsnClientState.ASLEEP); + client.wake(15000); + assertClientSessionState(client, MqttsnClientState.ASLEEP); + } + } + + @Test + public void testClientSleepConnect() throws IOException, MqttsnException, MqttsnClientConnectException { + try (MqttsnClient client = new MqttsnClient()) { + client.start(createClientRuntimeRegistry("testClientId")); + client.connect(CONNECT_TIMEOUT, true); + assertClientSessionState(client, MqttsnClientState.CONNECTED); + client.sleep(CONNECT_TIMEOUT); + assertClientSessionState(client, MqttsnClientState.ASLEEP); + client.connect(CONNECT_TIMEOUT, false); + assertClientSessionState(client, MqttsnClientState.CONNECTED); + } + } + + @Test + public void testMultipleClientConnections() throws Exception, MqttsnClientConnectException { + + final int concurrent = 5; + final CountDownLatch latch = new CountDownLatch(concurrent); + final Runnable r = new Runnable() { + @Override + public void run() { + try { + final CountDownLatch localLatch = new CountDownLatch(1); + final byte[] payload = "hello".getBytes(); + try (MqttsnClient client = new MqttsnClient()) { + client.start(createClientRuntimeRegistry("testClientId")); + client.registerReceivedListener((context, topicName, qos, data) -> { + if(Objects.deepEquals(data, payload)){ + latch.countDown(); + localLatch.countDown(); + } + }); + client.connect(CONNECT_TIMEOUT, true); + final String publishTopic = String.format(TOPIC, client.getClientId()); + client.subscribe(publishTopic, 2); + assertClientSessionState(client, MqttsnClientState.CONNECTED); + client.publish(publishTopic,2, + payload); + localLatch.await(MUTLI_CLIENT_LATCH_TIMEOUT, TimeUnit.SECONDS); + } + } catch(Exception e){ + e.printStackTrace(); + } + } + }; + for (int i = 0; i < concurrent; i++){ + Thread t = new Thread(r); + t.start(); + } + + Assert.assertTrue("timedout waiting for all clients", latch.await(MUTLI_CLIENT_LATCH_TIMEOUT, TimeUnit.SECONDS)); + } + + protected void assertClientSessionState(MqttsnClient client, MqttsnClientState state){ + IMqttsnSessionState s = client.getSessionState(); + Assert.assertNotNull("session state should not be null", s); + MqttsnClientState sessionClientState = s.getClientState(); + if(state != null){ + Assert.assertNotNull("client state should not be null", sessionClientState); + Assert.assertEquals("client state should match", state, sessionClientState); + } else { + Assert.assertNull("client state should be null", s.getClientState()); + } + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/README.md b/mqtt-sn-codec/README.md new file mode 100644 index 00000000..a4b737a6 --- /dev/null +++ b/mqtt-sn-codec/README.md @@ -0,0 +1,28 @@ +# mqtt-sn-codec +Full, dependency free java implementation of the MQTT-SN protocol specification. Abstraction of the underlying model to support pluggable wire protocols. + +```java + public class ExampleUsage { + public static void main(String[] args) throws Exception { + + //-- select the version of the protocol you wish to use. The versions + //-- registered on the interface are thread-safe, pre-constructed singletons. + //-- you can also manually construct your codecs and manage their lifecycle if you so desire. + IMqttsnCodec mqttsnVersion1_2 = MqttsnCodecs.MQTTSN_CODEC_VERSION_1_2; + + //-- each codec supplies a message factory allowing convenient construction of messages for use + //-- in your application. + IMqttsnMessageFactory factory = mqttsnVersion1_2.createMessageFactory(); + + //-- construct a connect message with your required configuration + IMqttsnMessage connect = + factory.createConnect("testClientId", 60, false, true); + + //-- encode the connect message for wire transport... + byte[] arr = mqttsnVersion1_2.encode(connect); + + //-- then on the other side of the wire.. + connect = mqttsnVersion1_2.decode(arr); + } + } +``` \ No newline at end of file diff --git a/mqtt-sn-codec/pom.xml b/mqtt-sn-codec/pom.xml new file mode 100644 index 00000000..9c9fac2a --- /dev/null +++ b/mqtt-sn-codec/pom.xml @@ -0,0 +1,61 @@ + + + + + 4.0.0 + + org.slj + mqtt-sn-codec + 1.0.0 + + + UTF-8 + 4.13 + 3.8.1 + 1.8 + 1.8 + + + + + junit + junit + ${junit.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/ExampleUsage.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/ExampleUsage.java new file mode 100644 index 00000000..8e31df1c --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/ExampleUsage.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn; + +import org.slj.mqtt.sn.codec.MqttsnCodecs; +import org.slj.mqtt.sn.spi.IMqttsnCodec; +import org.slj.mqtt.sn.spi.IMqttsnMessage; +import org.slj.mqtt.sn.spi.IMqttsnMessageFactory; + +public class ExampleUsage { + + public static void main(String[] args) throws Exception { + + //-- select the version of the protocol you wish to use. The versions + //-- registered on the interface are thread-safe, pre-constructed singletons. + //-- you can also manually construct your codecs and manage their lifecycle if you so desire. + IMqttsnCodec mqttsnVersion1_2 = MqttsnCodecs.MQTTSN_CODEC_VERSION_1_2; + + //-- each codec supplies a message factory allowing convenient construction of messages for use + //-- in your application. + IMqttsnMessageFactory factory = mqttsnVersion1_2.createMessageFactory(); + + //-- construct a connect message with your required configuration + IMqttsnMessage connect = + factory.createConnect("testClientId", 60, false, true); + + System.out.println(connect); + + //-- encode the connect message for wire transport... + byte[] arr = mqttsnVersion1_2.encode(connect); + + //-- then on the other side of the wire.. + connect = mqttsnVersion1_2.decode(arr); + + System.out.println(connect); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/MqttsnConstants.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/MqttsnConstants.java new file mode 100644 index 00000000..dba96d64 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/MqttsnConstants.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public interface MqttsnConstants { + + String ENCODING = "UTF-8"; + Charset CHARSET = StandardCharsets.UTF_8; + + int USIGNED_MAX_16 = 65535; + int USIGNED_MAX_8 = 255; + + byte TOPIC_NORMAL = 0b00, + TOPIC_PREDEFINED = 0b01, + TOPIC_SHORT = 0b10, + TOPIC_RESERVED = 0b11; + + enum TOPIC_TYPE { + + NORMAL(TOPIC_NORMAL), + PREDEFINED(TOPIC_PREDEFINED), + SHORT(TOPIC_SHORT), + RESERVED(TOPIC_RESERVED); + + byte flag; + + TOPIC_TYPE(byte flag) { + this.flag = flag; + } + + public byte getFlag() { + return flag; + } + } + + int RETURN_CODE_ACCEPTED = 0x00, + RETURN_CODE_REJECTED_CONGESTION = 0x01, + RETURN_CODE_INVALID_TOPIC_ID = 0x02, + RETURN_CODE_SERVER_UNAVAILABLE = 0x03; + + int QoS0 = 0, + QoS1 = 1, + QoS2 = 2, + QoSM1 = -1; + + byte ADVERTISE = 0x00; + byte SEARCHGW = 0x01; + byte GWINFO = 0x02; + byte CONNECT = 0x04; + byte CONNACK = 0x05; + byte WILLTOPICREQ = 0x06; + byte WILLTOPIC = 0x07; + byte WILLMSGREQ = 0x08; + byte WILLMSG = 0x09; + byte REGISTER = 0x0A; + byte REGACK = 0x0B; + byte PUBLISH = 0x0C; + byte PUBACK = 0x0D; + byte PUBCOMP = 0x0E; + byte PUBREC = 0x0F; + byte PUBREL = 0x10; + byte SUBSCRIBE = 0x12; + byte SUBACK = 0x13; + byte UNSUBSCRIBE = 0x14; + byte UNSUBACK = 0x15; + byte PINGREQ = 0x16; + byte PINGRESP = 0x17; + byte DISCONNECT = 0x18; + byte WILLTOPICUPD = 0x1A; + byte WILLTOPICRESP = 0x1B; + byte WILLMSGUPD = 0x1C; + byte WILLMSGRESP = 0x1D; + int ENCAPSMSG = 0xFE; + +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/PublishData.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/PublishData.java new file mode 100644 index 00000000..bb412317 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/PublishData.java @@ -0,0 +1,58 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn; + +public class PublishData { + + String topicPath; + int qos; + byte[] data; + + public PublishData(int qos, byte[] data) { + this.qos = qos; + this.data = data; + } + + public PublishData(String topicPath, int qos, byte[] data) { + this.topicPath = topicPath; + this.qos = qos; + this.data = data; + } + + public String getTopicPath() { + return topicPath; + } + + public int getQos() { + return qos; + } + + public byte[] getData() { + return data; + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/codec/AbstractMqttsnCodec.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/codec/AbstractMqttsnCodec.java new file mode 100644 index 00000000..1ad9dd09 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/codec/AbstractMqttsnCodec.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.codec; + +import org.slj.mqtt.sn.spi.IMqttsnCodec; +import org.slj.mqtt.sn.spi.IMqttsnMessage; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; +import org.slj.mqtt.sn.wire.version1_2.payload.AbstractMqttsnMessage; + +/** + * Base class for simple codec implementations. This version will only support message + * types defined by the local abstract, the marker interfaces allow support for any + * message implementations. + * + * @author Simon Johnson + */ +public abstract class AbstractMqttsnCodec implements IMqttsnCodec { + + @Override + public IMqttsnMessage decode(byte[] data) throws MqttsnCodecException { + IMqttsnMessage msg = createInstance(data); + if (!AbstractMqttsnMessage.class.isAssignableFrom(msg.getClass())) + throw new MqttsnCodecException("unsupported message formats in codec"); + ((AbstractMqttsnMessage) msg).decode(data); + return msg; + } + + @Override + public byte[] encode(IMqttsnMessage msg) throws MqttsnCodecException { + if (!AbstractMqttsnMessage.class.isAssignableFrom(msg.getClass())) + throw new MqttsnCodecException("unsupported message formats in codec"); + return ((AbstractMqttsnMessage) msg).encode(); + } + + protected void validateLengthEquals(byte[] data, int length) throws MqttsnCodecException { + if (data.length != length) { + throw new MqttsnCodecException( + String.format("invalid data length %s, must be %s bytes", data.length, length)); + } + } + + protected void validateLengthGreaterThanOrEquals(byte[] data, int length) throws MqttsnCodecException { + if (data.length < length) { + throw new MqttsnCodecException( + String.format("invalid data length %s, must be gt or eq to %s bytes", data.length, length)); + } + } + + @Override + public String print(IMqttsnMessage message) throws MqttsnCodecException { + return MqttsnWireUtils.toBinary(encode(message)); + } + + protected abstract IMqttsnMessage createInstance(byte[] data) throws MqttsnCodecException; +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/codec/AbstractMqttsnMessageFactory.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/codec/AbstractMqttsnMessageFactory.java new file mode 100644 index 00000000..eba8c0ec --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/codec/AbstractMqttsnMessageFactory.java @@ -0,0 +1,200 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.codec; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.spi.IMqttsnMessage; +import org.slj.mqtt.sn.spi.IMqttsnMessageFactory; + +public abstract class AbstractMqttsnMessageFactory implements IMqttsnMessageFactory { + + @Override + public IMqttsnMessage createAdvertise(int gatewayId, int duration) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createSearchGw(int radius) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createGwinfo(int gatewayId, String gatewayAddress) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createConnect(String clientId, int keepAlive, boolean willPrompt, boolean cleanSession) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createConnack(int returnCode) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createWillTopicReq() throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createWillTopic(int QoS, boolean retain, String topicPath) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createWillTopicResp(int returnCode) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createWillTopicupd(int QoS, boolean retain, String topicPath) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createWillMsgupd(byte[] payload) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createWillMsgReq() throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createWillMsg(byte[] payload) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createWillMsgResp(int returnCode) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createRegister(String topicPath) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createRegister(int topicAlias, String topicPath) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createRegack(int topicAlias, int returnCode) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createPublish(int QoS, boolean DUP, boolean retain, MqttsnConstants.TOPIC_TYPE type, int topicId, byte[] payload) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createPublish(int QoS, boolean DUP, boolean retain, String topicPath, byte[] payload) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createPuback(int topicId, int returnCode) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createPubrec() throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createPubrel() throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createPubcomp() throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createSubscribe(int QoS, MqttsnConstants.TOPIC_TYPE type, int topicId) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createSubscribe(int QoS, String topicName) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createSuback(int grantedQoS, int topicId, int returnCode) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createUnsubscribe(MqttsnConstants.TOPIC_TYPE type, int topicId) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createUnsubscribe(String topicName) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createUnsuback() throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createPingreq(String clientId) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createPingresp() throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createDisconnect() throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createDisconnect(int duration) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } + + @Override + public IMqttsnMessage createEncapsulatedMessage(String wirelessNodeId, int radius, byte[] messageData) throws MqttsnCodecException { + throw new UnsupportedOperationException("message not supported by codec"); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/codec/MqttsnCodecException.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/codec/MqttsnCodecException.java new file mode 100644 index 00000000..6b37caa8 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/codec/MqttsnCodecException.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.codec; + +public class MqttsnCodecException extends RuntimeException { + + public MqttsnCodecException() { + } + + public MqttsnCodecException(String message) { + super(message); + } + + public MqttsnCodecException(String message, Throwable cause) { + super(message, cause); + } + + public MqttsnCodecException(Throwable cause) { + super(cause); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/codec/MqttsnCodecs.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/codec/MqttsnCodecs.java new file mode 100644 index 00000000..d711efe1 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/codec/MqttsnCodecs.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.codec; + +import org.slj.mqtt.sn.spi.IMqttsnCodec; +import org.slj.mqtt.sn.wire.version1_2.Mqttsn_v1_2_Codec; + +public interface MqttsnCodecs { + + /** + * Version 1.2 support of the MQTT-SN specification + * + * @see Mqttsn specification at http://www.mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf + */ + IMqttsnCodec MQTTSN_CODEC_VERSION_1_2 = new Mqttsn_v1_2_Codec(); + +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/spi/IMqttsnCodec.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/spi/IMqttsnCodec.java new file mode 100644 index 00000000..29fef816 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/spi/IMqttsnCodec.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.PublishData; +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +/** + * A codec contains all the functionality to marshall and unmarshall + * wire traffic in the format specified by the implementation. Further, + * it also provides a message factory instance which allows construction + * of wire messages hiding the underlying transport format. This allows versioned + * protocol support. + * + * @author Simon Johnson + */ +public interface IMqttsnCodec { + + /** + * @return - Does the message represent a PUBLISH + */ + PublishData getData(IMqttsnMessage message); + + /** + * @return - Does the message represent a PUBLISH + */ + boolean isPublish(IMqttsnMessage message); + + /** + * @return - Does the message represent a PUBACK + */ + boolean isPuback(IMqttsnMessage message); + + /** + * @return - Does the message represent a PUBREL + */ + boolean isPubRel(IMqttsnMessage message); + + /** + * @return - Does the message represent a PUBREC + */ + boolean isPubRec(IMqttsnMessage message); + + /** + * @return - Does the message represent a DISCONNECT + */ + boolean isDisconnect(IMqttsnMessage message); + + /** + * Is the message considered an ACTIVE message, that is a message actively sent by an application + * (anything other than a PINGREQ, PINGRESP, DISCONNECT) + * @param message - the message to consider + * @return Is the message considered an ACTIVE message + */ + boolean isActiveMessage(IMqttsnMessage message); + + /** + * To help with debugging, this method will return a binary or hex + * representation of the encoded message + */ + String print(IMqttsnMessage message) throws MqttsnCodecException; + + /** + * Given data of the wire, will convert to the message model which can be used + * in a given runtime + * + * @throws MqttsnCodecException - something went wrong when decoding the data + */ + IMqttsnMessage decode(byte[] data) throws MqttsnCodecException; + + /** + * When supplied with messages constructed from an associated message factory, + * will encode them into data that can be sent on the wire + * + * @throws MqttsnCodecException - something went wrong when encoding the data + */ + byte[] encode(IMqttsnMessage message) throws MqttsnCodecException; + + /** + * A message factory will contruct messages using convenience methods + * that hide the complexity of the underlying wire format + */ + IMqttsnMessageFactory createMessageFactory(); + + /** + * Using the first few bytes of a message, determine the length of the full message, + * for use with stream reading + */ + int readMessageSize(byte[] arr) throws MqttsnCodecException; +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/spi/IMqttsnIdentificationPacket.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/spi/IMqttsnIdentificationPacket.java new file mode 100644 index 00000000..dcc35e79 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/spi/IMqttsnIdentificationPacket.java @@ -0,0 +1,6 @@ +package org.slj.mqtt.sn.spi; + +public interface IMqttsnIdentificationPacket { + + String getClientId(); +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessage.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessage.java new file mode 100644 index 00000000..280cdc7e --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessage.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.slj.mqtt.sn.spi; + + +import java.io.Serializable; + +/** + * Represents a wire message that can be manipulated by a gateway OR client for transport by + * changing its msgId. + * + * @author Simon Johnson + * @see Mqttsn specification at http://www.mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf + */ +public interface IMqttsnMessage extends Serializable { + + /** + * Get a user friendly version of the Message object. + * For example; + * "MqttsnConnect" + * "MqttsnDisconnect" + */ + String getMessageName(); + + /** + * If the underlying message uses a msg id as part of its contract + */ + boolean needsMsgId(); + + void setMsgId(int msgId); + + int getMsgId(); + + int getMessageType(); + + int getReturnCode(); + + boolean isErrorMessage(); +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageFactory.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageFactory.java new file mode 100644 index 00000000..4f28773d --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageFactory.java @@ -0,0 +1,392 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +/** + * Use to obtain instances of the codec messages for use in a client or gateway runtime. + * The lightweight abstraction allows message implementations & versions to be pluggable + * into 3rd party systems. + * + * @author Simon Johnson + * @see mqttsn specification at http://www.mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf + */ +public interface IMqttsnMessageFactory { + + /** + * The ADVERTISE message is broadcasted periodically by a gateway to advertise its presence. + * The time interval until the next broadcast time is indicated in the Duration field of this message. + * Its format is illustrated in Table 6 of the specification. + * + * @Param gatewayId: the id of the gateway which sends this message. + * @Param: duration time interval until the next ADVERTISE is broadcasted by this gateway. + */ + IMqttsnMessage createAdvertise(int gatewayId, int duration) + throws MqttsnCodecException; + + /** + * The SEARCHGW message is broadcasted by a client when it searches for a GW. The broadcast radius + * of the SEARCHGW is limited and depends on the density of the clients deployment, e.g. only 1-hop + * broadcast in case of a very dense network in which every MQTT-SN client is reachable from each + * other within 1-hop transmission. + * + * @param radius: the broadcast radius of this message. + * The broadcast radius is also indicated to the underlying network layer when MQTT-SN gives this message for transmission. + */ + IMqttsnMessage createSearchGw(int radius) + throws MqttsnCodecException; + + /** + * The GWINFO message is sent as response to a SEARCHGW message using the broadcast service of the underlying layer, + * with the radius as indicated in the SEARCHGW message. If sent by a GW, it contains only the id of the sending GW; + * otherwise, if sent by a client, it also includes the address of the GW. + * + * @param gatewayId: the id of a GW. + * @param gatewayAddress: address of the indicated GW; optional, only included if message is sent by a client. + * Like the SEARCHGW message the broadcast radius for this message is also indicated to the underlying + * network layer when MQTT-SN gives this message for transmission. + */ + IMqttsnMessage createGwinfo(int gatewayId, String gatewayAddress) + throws MqttsnCodecException; + + /** + * The CONNECT message is sent by a client to setup a connection. + * + * @param clientId: same as with MQTT, contains the client id which is a 1-23 character long string which uniquely identifies the client to the server. + * @param keepAlive: same as with MQTT, contains the value of the Keep Alive timer. + * @param willPrompt: if set, indicates that client is requesting for Will topic and Will message prompting. + * @param cleanSession: same meaning as with MQTT, however extended for Will topic and Will message. + */ + IMqttsnMessage createConnect(String clientId, int keepAlive, boolean willPrompt, boolean cleanSession) + throws MqttsnCodecException; + + /** + * The CONNACK message is sent by the server in response to a connection request from a client. + *

+ * Allowed Return code(s): + * 0x00 - Accepted + * 0x01 - Rejected: congestion + * 0x02 - Rejected: invalid topic ID + * 0x03 - Rejected: not supported reserved + * 0x04 - 0xFF: reserved + * + * @param returnCode: see above + */ + IMqttsnMessage createConnack(int returnCode) + throws MqttsnCodecException; + + /** + * The WILLTOPICREQ message is sent by the GW to request a client for sending the Will topic name. + */ + IMqttsnMessage createWillTopicReq() + throws MqttsnCodecException; + + /** + * The WILLTOPIC message is sent by a client as response to the WILLTOPICREQ message for transferring + * its Will topic name to the GW. + *

+ * An empty WILLTOPIC message is a WILLTOPIC message without Flags and WillTopic field (i.e. it is exactly 2 octets long). + * It is used by a client to delete the Will topic and the Will message stored in the server, see Section 6.4. + * + * @param QoS: same as MQTT, contains the Will QoS + * @param retain: same as MQTT, contains the Will Retain flag – Will: not used + * @param topicPath: contains the Will topic name. + */ + IMqttsnMessage createWillTopic(int QoS, boolean retain, String topicPath) + throws MqttsnCodecException; + + /** + * The WILLTOPICRESP message is sent by a GW to acknowledge the receipt and processing of an WILLTOPICUPD message. + *

+ * Allowed Return code(s): + * 0x00 - Accepted + * 0x01 - Rejected: congestion + * 0x02 - Rejected: invalid topic ID + * 0x03 - Rejected: not supported reserved + * 0x04 - 0xFF: reserved + * + * @param returnCode: see above + */ + IMqttsnMessage createWillTopicResp(int returnCode) + throws MqttsnCodecException; + + /** + * The WILLTOPICUPD message is sent by a client to update its Will topic name stored in the GW/server. + * An empty WILLTOPICUPD message is a WILLTOPICUPD message without Flags and WillTopic field (i.e. + * it is exactly 2 octets long). It is used by a client to delete its Will topic and Will message stored in the GW/server. + * + * @param QoS same as MQTT, contains the Will QoS + * @param retain same as MQTT, contains the Will Retain flag – Will: not used + * @param topicPath contains the Will topic name. + */ + IMqttsnMessage createWillTopicupd(int QoS, boolean retain, String topicPath) + throws MqttsnCodecException; + + /** + * The WILLMSGUPD message is sent by a client to update its Will message stored in the GW/server. + * + * @param payload contains the Will message. + */ + IMqttsnMessage createWillMsgupd(byte[] payload) + throws MqttsnCodecException; + + /** + * The WILLMSGREQ message is sent by the GW to request a client for sending the Will message. + */ + IMqttsnMessage createWillMsgReq() + throws MqttsnCodecException; + + /** + * The WILLMSG message is sent by a client as response to a WILLMSGREQ for transferring its Will message to the GW. + * + * @param payload: contains the Will message. + */ + IMqttsnMessage createWillMsg(byte[] payload) + throws MqttsnCodecException; + + /** + * The WILLMSGRESP message is sent by a GW to acknowledge the receipt and processing of an WILLMSGUPD message. + *

+ * Allowed Return code(s): + * 0x00 - Accepted + * 0x01 - Rejected: congestion + * 0x02 - Rejected: invalid topic ID + * 0x03 - Rejected: not supported reserved + * 0x04 - 0xFF: reserved + * + * @param returnCode: see above + */ + IMqttsnMessage createWillMsgResp(int returnCode) + throws MqttsnCodecException; + + /** + * The REGISTER message is sent by a client to a GW for requesting a topic id value for the included topic name. + * It is also sent by a GW to inform a client about the topic id value it has assigned to the included topic name. + * + * @param topicPath: contains the topic name. + */ + IMqttsnMessage createRegister(String topicPath) + throws MqttsnCodecException; + + /** + * The REGISTER message is sent by a client to a GW for requesting a topic id value for the included topic name. + * It is also sent by a GW to inform a client about the topic id value it has assigned to the included topic name. + * + * @param topicAlias: if sent by a client, it is coded 0x0000 and is not relevant; if sent by a GW, it contains the topic id + * value assigned to the topic name included in the TopicName field; + * @param topicPath: contains the topic name. + */ + IMqttsnMessage createRegister(int topicAlias, String topicPath) + throws MqttsnCodecException; + + /** + * The REGACK message is sent by a client or by a GW as an acknowledgment to the receipt and processing of a REGISTER message. + *

+ * Allowed Return code(s): + * 0x00 - Accepted + * 0x01 - Rejected: congestion + * 0x02 - Rejected: invalid topic ID + * 0x03 - Rejected: not supported reserved + * 0x04 - 0xFF: reserved + * + * @param topicAlias: the value that shall be used as topic id in the PUBLISH messages; + * @param returnCode + */ + IMqttsnMessage createRegack(int topicAlias, int returnCode) + throws MqttsnCodecException; + + + /** + * This message is used by both clients and gateways to publish data for a certain topic. + * + * @param DUP: same as MQTT, indicates whether message is sent for the first time or not. + * @param QoS: same as MQTT, contains the QoS level for this PUBLISH message. + * @param retain: same as MQTT, contains the Retain flag. + * @param type: indicates the type of the topic id contained in the TopicId field. + * @param topicId: contains the topic id value or the short topic name for which the data is published. + * @param payload: the published data + */ + IMqttsnMessage createPublish(int QoS, boolean DUP, boolean retain, MqttsnConstants.TOPIC_TYPE type, int topicId, byte[] payload) + throws MqttsnCodecException; + + /** + * This message is used by both clients and gateways to publish data for topics with short names + * + * @param DUP: same as MQTT, indicates whether message is sent for the first time or not. + * @param QoS: same as MQTT, contains the QoS level for this PUBLISH message. + * @param retain: same as MQTT, contains the Retain flag. + * @param topicPath: the SHORT topic path - version 1.2 does not support full topicNames in the publish + * @param payload: the published data + */ + IMqttsnMessage createPublish(int QoS, boolean DUP, boolean retain, String topicPath, byte[] payload) throws MqttsnCodecException; + + /** + * The PUBACK message is sent by a gateway or a client as an acknowledgment to the receipt and processing + * of a PUBLISH message in case of QoS levels 1 or 2. It can also be sent as response to a PUBLISH message + * in case of an error; the error reason is then indicated in the ReturnCode field. + *

+ * Allowed Return code(s): + * 0x00 - Accepted + * 0x01 - Rejected: congestion + * 0x02 - Rejected: invalid topic ID + * 0x03 - Rejected: not supported reserved + * 0x04 - 0xFF: reserved + * + * @param topicId: same value the one contained in the corresponding PUBLISH message. + * @param returnCode - see table + */ + IMqttsnMessage createPuback(int topicId, int returnCode) + throws MqttsnCodecException; + + /** + * As with MQTT, the PUBREC, PUBREL, and PUBCOMP messages are used in conjunction with a PUBLISH message with QoS level 2. + */ + IMqttsnMessage createPubrec() + throws MqttsnCodecException; + + /** + * As with MQTT, the PUBREC, PUBREL, and PUBCOMP messages are used in conjunction with a PUBLISH message with QoS level 2. + */ + IMqttsnMessage createPubrel() + throws MqttsnCodecException; + + /** + * As with MQTT, the PUBREC, PUBREL, and PUBCOMP messages are used in conjunction with a PUBLISH message with QoS level 2. + */ + IMqttsnMessage createPubcomp() + throws MqttsnCodecException; + + /** + * The SUBSCRIBE message is used by a client to subscribe to a certain topic name. + * NB: this method should ONLY be used to set topicAlias to either PREDEFINED or NORMAL + * + * @param QoS: same as MQTT, contains the requested QoS level for this topic. + * @param topicId: topic id + */ + IMqttsnMessage createSubscribe(int QoS, MqttsnConstants.TOPIC_TYPE type, int topicId) + throws MqttsnCodecException; + + /** + * The SUBSCRIBE message is used by a client to subscribe to a certain topic name. + * + * @param QoS: same as MQTT, contains the requested QoS level for this topic. + * @param topicName: topic path to subscribe to + */ + IMqttsnMessage createSubscribe(int QoS, String topicName) + throws MqttsnCodecException; + + /** + * The SUBACK message is sent by a gateway to a client as an acknowledgment to the receipt and processing of a SUBSCRIBE message. + *

+ * Allowed Return code(s): + * 0x00 - Accepted + * 0x01 - Rejected: congestion + * 0x02 - Rejected: invalid topic ID + * 0x03 - Rejected: not supported reserved + * 0x04 - 0xFF: reserved + * + * @param grantedQoS: same as MQTT, contains the granted QoS level. + * @param topicId: in case of "accepted" the value that will be used as topicid by the gateway when sending PUBLISH messages to the client + * (not relevant in case of subscriptions to a short topic name or to a topic name which contains wildcard characters) + * @param returnCode - see table + */ + IMqttsnMessage createSuback(int grantedQoS, int topicId, int returnCode) + throws MqttsnCodecException; + + /** + * An UNSUBSCRIBE message is sent by the client to the GW to unsubscribe from named topics. + * + * @param type: indicates the type of the topic id contained in the TopicId field. + * @param topicId the topicAlias + */ + IMqttsnMessage createUnsubscribe(MqttsnConstants.TOPIC_TYPE type, int topicId) + throws MqttsnCodecException; + + /** + * An UNSUBSCRIBE message is sent by the client to the GW to unsubscribe from named topics. + * + * @param topicName the topicName + */ + IMqttsnMessage createUnsubscribe(String topicName) + throws MqttsnCodecException; + + /** + * An UNSUBACK message is sent by a GW to acknowledge the receipt and processing of an UNSUBSCRIBE + */ + IMqttsnMessage createUnsuback() + throws MqttsnCodecException; + + /** + * As with MQTT, the PINGREQ message is an ”are you alive” message that is sent from or received by a connected client. + * + * @param clientId: contains the client id; this field is optional and is included by a "sleeping" client when + * it goes to the "awake" state and is waiting for messages sent by the server/gateway. + */ + IMqttsnMessage createPingreq(String clientId) + throws MqttsnCodecException; + + /** + * As with MQTT, a PINGRESP message is the response to a PINGREQ message and means ”yes I am alive”. Keep Alive messages + * flow in either direction, sent either by a connected client or the gateway. + * Moreover, a PINGRESP message is sent by a gateway to inform a sleeping client that it has no more buffered messages + * for that client. + */ + IMqttsnMessage createPingresp() + throws MqttsnCodecException; + + /** + * As with MQTT, the DISCONNECT message is sent by a client to indicate that it wants to close the connection. + * The gateway will acknowledge the receipt of that message by returning a DISCONNECT to the client. + * A server or gateway may also sends a DISCONNECT to a client, e.g. in case a gateway, due to an + * error, cannot map a received message to a client. Upon receiving such a DISCONNECT message, + * a client should try to setup the connection again by sending a CONNECT message to the gateway or server. + * In all these cases the DISCONNECT message does not contain the Duration field. + */ + IMqttsnMessage createDisconnect() + throws MqttsnCodecException; + + /** + * A DISCONNECT message with a Duration field is sent by a client when it wants to go to the "asleep" state. + * The receipt of this message is also acknowledged by the gateway by means of a DISCONNECT message + * (without a duration field). + * + * @param duration - length of sleeping session + */ + IMqttsnMessage createDisconnect(int duration) + throws MqttsnCodecException; + + /** + * Encapsulate message for use on forwarders. + * + * @param wirelessNodeId - Wireless Node Id: identifies the wireless node which has sent or should receive the encapsulated MQTT-SN message. + * The mapping between this Id and the address of the wireless node is implemented by the forwarder, if needed. + * @param radius - broadcast radius (only relevant in direction GW to forwarder) + * @param messageData - the MQTT-SN message to forward + */ + IMqttsnMessage createEncapsulatedMessage(String wirelessNodeId, int radius, byte[] messageData) + throws MqttsnCodecException; +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/MqttsnWireUtils.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/MqttsnWireUtils.java new file mode 100644 index 00000000..4a605429 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/MqttsnWireUtils.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +public class MqttsnWireUtils { + + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + + public static void validate8Bit(int field) throws MqttsnCodecException { + if (field < 0 || field > MqttsnConstants.USIGNED_MAX_8) { + throw new MqttsnCodecException("invalid unsigned 8 bit number - " + field); + } + } + + public static void validate16Bit(int field) throws MqttsnCodecException { + if (field < 0 || field > MqttsnConstants.USIGNED_MAX_16) { + throw new MqttsnCodecException("invalid unsigned 16 bit number - " + field); + } + } + + public static void validateQoS(int QoS) throws MqttsnCodecException { + if (QoS != MqttsnConstants.QoSM1 && + QoS != MqttsnConstants.QoS0 && + QoS != MqttsnConstants.QoS1 && + QoS != MqttsnConstants.QoS2) { + throw new MqttsnCodecException("invalid QoS number - " + QoS); + } + } + + public static int read8bit(byte b1) { + return (b1 & 0xFF); + } + + public static int read16bit(byte b1, byte b2) { + return ((b1 & 0xFF) << 8) + (b2 & 0xFF); + } + + public static String toBinary(byte... b) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; b != null && i < b.length; i++) { + sb.append(String.format("%8s", Integer.toBinaryString(b[i] & 0xFF)).replace(' ', '0')); + if (i < b.length - 1) + sb.append(" "); + } + return sb.toString(); + } + + public static String toHex(byte... b) { + if(b == null) return null; + char[] hexChars = new char[b.length * 2]; + for (int j = 0; j < b.length; j++) { + int v = b[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + + public static byte[] readBuffer(byte[] buf, int off, int len){ + int copyLength = Math.min(len, buf.length); + byte[] copy = new byte[copyLength]; + System.arraycopy(buf, off, copy, 0, copyLength); + return copy; + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/Mqttsn_v1_2_Codec.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/Mqttsn_v1_2_Codec.java new file mode 100644 index 00000000..fcfb76ac --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/Mqttsn_v1_2_Codec.java @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.PublishData; +import org.slj.mqtt.sn.codec.AbstractMqttsnCodec; +import org.slj.mqtt.sn.codec.MqttsnCodecException; +import org.slj.mqtt.sn.spi.IMqttsnMessage; +import org.slj.mqtt.sn.spi.IMqttsnMessageFactory; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; +import org.slj.mqtt.sn.wire.version1_2.payload.*; + +public class Mqttsn_v1_2_Codec extends AbstractMqttsnCodec { + + protected IMqttsnMessageFactory messageFactory; + + @Override + public PublishData getData(IMqttsnMessage message) { + MqttsnPublish publish = (MqttsnPublish) message ; + return new PublishData(publish.getQoS(), publish.getData()); + } + + @Override + public boolean isDisconnect(IMqttsnMessage message) { + return message instanceof MqttsnDisconnect; + } + + @Override + public boolean isPublish(IMqttsnMessage message) { + return message instanceof MqttsnPublish; + } + + @Override + public boolean isPuback(IMqttsnMessage message) { + return message instanceof MqttsnPuback; + } + + @Override + public boolean isPubRel(IMqttsnMessage message) { + return message instanceof MqttsnPubrel; + } + + @Override + public boolean isPubRec(IMqttsnMessage message) { + return message instanceof MqttsnPubrec; + } + + @Override + public boolean isActiveMessage(IMqttsnMessage message) { + return ! (message instanceof MqttsnPingreq || + message instanceof MqttsnDisconnect || message instanceof MqttsnPingresp); + } + + @Override + public int readMessageSize(byte[] data) throws MqttsnCodecException { + if (data == null || data.length == 0) + throw new MqttsnCodecException("malformed mqtt-sn packet, need at least 1 byte for sizing"); + return readMessageLength(data); + } + + protected AbstractMqttsnMessage createInstance(byte[] data) throws MqttsnCodecException { + + if (data == null || data.length < 2) + throw new MqttsnCodecException("malformed mqtt-sn packet"); + + AbstractMqttsnMessage msg = null; + int msgType = readMessageType(data); + + switch (msgType) { + case MqttsnConstants.ADVERTISE: + validateLengthGreaterThanOrEquals(data, 3); + msg = new MqttsnAdvertise(); + break; + case MqttsnConstants.SEARCHGW: + validateLengthEquals(data, 3); + msg = new MqttsnSearchGw(); + break; + case MqttsnConstants.GWINFO: + validateLengthGreaterThanOrEquals(data, 3); + msg = new MqttsnGwInfo(); + break; + case MqttsnConstants.CONNECT: + validateLengthGreaterThanOrEquals(data, 6); + msg = new MqttsnConnect(); + break; + case MqttsnConstants.CONNACK: + validateLengthEquals(data, 3); + msg = new MqttsnConnack(); + break; + case MqttsnConstants.REGISTER: + validateLengthGreaterThanOrEquals(data, 7); + msg = new MqttsnRegister(); + break; + case MqttsnConstants.REGACK: + validateLengthEquals(data, 7); + msg = new MqttsnRegack(); + break; + case MqttsnConstants.PUBLISH: + validateLengthGreaterThanOrEquals(data, 7); + msg = new MqttsnPublish(); + msg.decode(data); + break; + case MqttsnConstants.PUBACK: + validateLengthEquals(data, 7); + msg = new MqttsnPuback(); + break; + case MqttsnConstants.PUBCOMP: + validateLengthEquals(data, 4); + msg = new MqttsnPubcomp(); + break; + case MqttsnConstants.PUBREC: + validateLengthEquals(data, 4); + msg = new MqttsnPubrec(); + break; + case MqttsnConstants.PUBREL: + validateLengthEquals(data, 4); + msg = new MqttsnPubrel(); + break; + case MqttsnConstants.PINGREQ: + validateLengthGreaterThanOrEquals(data, 2); + msg = new MqttsnPingreq(); + break; + case MqttsnConstants.PINGRESP: + validateLengthEquals(data, 2); + msg = new MqttsnPingresp(); + break; + case MqttsnConstants.DISCONNECT: + validateLengthGreaterThanOrEquals(data, 2); + msg = new MqttsnDisconnect(); + break; + case MqttsnConstants.SUBSCRIBE: + validateLengthGreaterThanOrEquals(data, 6); + msg = new MqttsnSubscribe(); + break; + case MqttsnConstants.SUBACK: + validateLengthEquals(data, 8); + msg = new MqttsnSuback(); + break; + case MqttsnConstants.UNSUBSCRIBE: + validateLengthGreaterThanOrEquals(data, 6); + msg = new MqttsnUnsubscribe(); + break; + case MqttsnConstants.UNSUBACK: + validateLengthEquals(data, 4); + msg = new MqttsnUnsuback(); + break; + case MqttsnConstants.WILLTOPICREQ: + validateLengthEquals(data, 2); + msg = new MqttsnWilltopicreq(); + break; + case MqttsnConstants.WILLTOPIC: + validateLengthGreaterThanOrEquals(data, 3); + msg = new MqttsnWilltopic(); + break; + case MqttsnConstants.WILLMSGREQ: + validateLengthEquals(data, 2); + msg = new MqttsnWillmsgreq(); + break; + case MqttsnConstants.WILLMSG: + validateLengthGreaterThanOrEquals(data, 2); + msg = new MqttsnWillmsg(); + break; + case MqttsnConstants.WILLTOPICUPD: + validateLengthGreaterThanOrEquals(data, 3); + msg = new MqttsnWilltopicudp(); + break; + case MqttsnConstants.WILLTOPICRESP: + validateLengthEquals(data, 3); + msg = new MqttsnWilltopicresp(); + break; + case MqttsnConstants.WILLMSGUPD: + validateLengthGreaterThanOrEquals(data, 2); + msg = new MqttsnWillmsgupd(); + break; + case MqttsnConstants.WILLMSGRESP: + validateLengthEquals(data, 3); + msg = new MqttsnWillmsgresp(); + break; + case MqttsnConstants.ENCAPSMSG: + validateLengthGreaterThanOrEquals(data, 5); + msg = new MqttsnEncapsmsg(); + break; + default: + throw new MqttsnCodecException(String.format("unknown message type [%s]", msgType)); + } + return msg; + } + + public static int readMessageType(byte[] data) { + int msgType; + if (isExtendedMessage(data)) { + msgType = (data[3] & 0xFF); + } else { + msgType = (data[1] & 0xFF); + } + return msgType; + } + + public static byte readHeaderByteWithOffset(byte[] data, int index) { + return isExtendedMessage(data) ? data[index + 2] : data[index]; + } + + public static boolean isExtendedMessage(byte[] data) { + return data[0] == 0x01; + } + + public static int readMessageLength(byte[] data) { + int length = 0; + if (isExtendedMessage(data)) { + //big payload + length = ((data[1] & 0xFF) << 8) + (data[2] & 0xFF); + } else { + //small payload + length = (data[0] & 0xFF); + } + return length; + } + + @Override + public IMqttsnMessageFactory createMessageFactory() { + if (messageFactory == null) { + synchronized (this) { + if (messageFactory == null) messageFactory = Mqttsn_v1_2_MessageFactory.getInstance(); + } + } + return messageFactory; + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/Mqttsn_v1_2_MessageFactory.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/Mqttsn_v1_2_MessageFactory.java new file mode 100644 index 00000000..59e12130 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/Mqttsn_v1_2_MessageFactory.java @@ -0,0 +1,391 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.AbstractMqttsnMessageFactory; +import org.slj.mqtt.sn.codec.MqttsnCodecException; +import org.slj.mqtt.sn.spi.IMqttsnMessage; +import org.slj.mqtt.sn.spi.IMqttsnMessageFactory; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; +import org.slj.mqtt.sn.wire.version1_2.payload.*; + +public class Mqttsn_v1_2_MessageFactory extends AbstractMqttsnMessageFactory implements IMqttsnMessageFactory { + + //singleton + private static Mqttsn_v1_2_MessageFactory instance; + + private Mqttsn_v1_2_MessageFactory() { + } + + public static IMqttsnMessageFactory getInstance() { + if (instance == null) { + synchronized (Mqttsn_v1_2_MessageFactory.class) { + if (instance == null) instance = new Mqttsn_v1_2_MessageFactory(); + } + } + return instance; + } + + @Override + public IMqttsnMessage createAdvertise(int gatewayId, int duration) throws MqttsnCodecException { + + MqttsnWireUtils.validate8Bit(gatewayId); + MqttsnWireUtils.validate16Bit(duration); + + MqttsnAdvertise msg = new MqttsnAdvertise(); + msg.setGatewayId(gatewayId); + msg.setDuration(duration); + return msg; + } + + @Override + public IMqttsnMessage createSearchGw(int radius) throws MqttsnCodecException { + + MqttsnWireUtils.validate8Bit(radius); + + MqttsnSearchGw msg = new MqttsnSearchGw(); + msg.setRadius(radius); + return msg; + } + + @Override + public IMqttsnMessage createGwinfo(int gatewayId, String gatewayAddress) throws MqttsnCodecException { + + MqttsnWireUtils.validate8Bit(gatewayId); + + MqttsnGwInfo msg = new MqttsnGwInfo(); + msg.setGatewayId(gatewayId); + msg.setGatewayAddress(gatewayAddress); + return msg; + } + + @Override + public IMqttsnMessage createConnect(String clientId, int keepAlive, boolean willPrompt, boolean cleanSession) throws MqttsnCodecException { + + MqttsnWireUtils.validate16Bit(keepAlive); + + MqttsnConnect msg = new MqttsnConnect(); + msg.setClientId(clientId); + msg.setDuration(keepAlive); + msg.setProtocolId(0); + msg.setCleanSession(cleanSession); + msg.setWill(willPrompt); + return msg; + } + + @Override + public IMqttsnMessage createConnack(int returnCode) throws MqttsnCodecException { + + MqttsnWireUtils.validate8Bit(returnCode); + + MqttsnConnack msg = new MqttsnConnack(); + msg.setReturnCode(returnCode); + return msg; + } + + @Override + public IMqttsnMessage createWillTopicReq() throws MqttsnCodecException { + MqttsnWilltopicreq msg = new MqttsnWilltopicreq(); + return msg; + } + + @Override + public IMqttsnMessage createWillTopic(int QoS, boolean retain, String topicPath) throws MqttsnCodecException { + MqttsnWilltopic msg = new MqttsnWilltopic(); + msg.setQoS(QoS); + msg.setRetainedPublish(retain); + msg.setWillTopic(topicPath); + return msg; + } + + @Override + public IMqttsnMessage createWillTopicResp(int returnCode) throws MqttsnCodecException { + MqttsnWilltopicresp msg = new MqttsnWilltopicresp(); + msg.setReturnCode(returnCode); + return msg; + } + + @Override + public IMqttsnMessage createWillTopicupd(int QoS, boolean retain, String topicPath) throws MqttsnCodecException { + MqttsnWilltopicudp msg = new MqttsnWilltopicudp(); + msg.setQoS(QoS); + msg.setRetainedPublish(retain); + msg.setWillTopic(topicPath); + return msg; + } + + @Override + public IMqttsnMessage createWillMsgupd(byte[] payload) throws MqttsnCodecException { + MqttsnWillmsgupd msg = new MqttsnWillmsgupd(); + msg.setMsgData(payload); + return msg; + } + + @Override + public IMqttsnMessage createWillMsgReq() throws MqttsnCodecException { + MqttsnWillmsgreq msg = new MqttsnWillmsgreq(); + return msg; + } + + @Override + public IMqttsnMessage createWillMsg(byte[] payload) throws MqttsnCodecException { + MqttsnWillmsg msg = new MqttsnWillmsg(); + msg.setMsgData(payload); + return msg; + } + + @Override + public IMqttsnMessage createWillMsgResp(int returnCode) throws MqttsnCodecException { + + MqttsnWireUtils.validate8Bit(returnCode); + + MqttsnWillmsgresp msg = new MqttsnWillmsgresp(); + msg.setReturnCode(returnCode); + return msg; + } + + @Override + public IMqttsnMessage createRegister(int topicAlias, String topicPath) throws MqttsnCodecException { + + MqttsnWireUtils.validate16Bit(topicAlias); + + MqttsnRegister msg = new MqttsnRegister(); + msg.setTopicId(topicAlias); + msg.setTopicName(topicPath); + return msg; + } + + @Override + public IMqttsnMessage createRegister(String topicPath) throws MqttsnCodecException { + MqttsnRegister msg = new MqttsnRegister(); + msg.setTopicName(topicPath); + return msg; + } + + @Override + public IMqttsnMessage createRegack(int topicAlias, int returnCode) throws MqttsnCodecException { + + MqttsnWireUtils.validate8Bit(returnCode); + MqttsnWireUtils.validate16Bit(topicAlias); + + MqttsnRegack msg = new MqttsnRegack(); + msg.setTopicId(topicAlias); + msg.setReturnCode(returnCode); + return msg; + } + + @Override + public IMqttsnMessage createPublish(int QoS, boolean DUP, boolean retain, MqttsnConstants.TOPIC_TYPE type, int topicId, byte[] payload) throws MqttsnCodecException { + + MqttsnWireUtils.validate16Bit(topicId); + + MqttsnPublish msg = new MqttsnPublish(); + msg.setQoS(QoS); + msg.setDupRedelivery(DUP); + msg.setRetainedPublish(retain); + msg.setData(payload); + switch (type) { + case NORMAL: + msg.setNormalTopicAlias(topicId); + break; + case PREDEFINED: + msg.setPredefinedTopicAlias(topicId); + break; + case SHORT: + byte[] topicData = new byte[2]; + topicData[0] = (byte) ((topicId >> 8) & 0xFF); + topicData[1] = (byte) (topicId & 0xFF); + msg.setTopicName(new String(topicData)); + break; + default: + throw new MqttsnCodecException("publish method only supports predefined and normal topic id types"); + } + return msg; + } + + @Override + public IMqttsnMessage createPublish(int QoS, boolean DUP, boolean retain, String topicPath, byte[] payload) throws MqttsnCodecException { + + int length = topicPath.getBytes(MqttsnConstants.CHARSET).length; + if (length > 2) + throw new MqttsnCodecException(String.format("invalid short topic supplied [%s] > 2", length)); + MqttsnPublish msg = new MqttsnPublish(); + msg.setQoS(QoS); + msg.setDupRedelivery(DUP); + msg.setRetainedPublish(retain); + msg.setData(payload); + msg.setTopicName(topicPath); + return msg; + } + + @Override + public IMqttsnMessage createPuback(int topicId, int returnCode) throws MqttsnCodecException { + + MqttsnWireUtils.validate8Bit(returnCode); + MqttsnWireUtils.validate16Bit(topicId); + + MqttsnPuback msg = new MqttsnPuback(); + msg.setTopicId(topicId); + msg.setReturnCode(returnCode); + return msg; + } + + @Override + public IMqttsnMessage createPubrec() throws MqttsnCodecException { + MqttsnPubrec msg = new MqttsnPubrec(); + return msg; + } + + @Override + public IMqttsnMessage createPubrel() throws MqttsnCodecException { + MqttsnPubrel msg = new MqttsnPubrel(); + return msg; + } + + @Override + public IMqttsnMessage createPubcomp() throws MqttsnCodecException { + MqttsnPubcomp msg = new MqttsnPubcomp(); + return msg; + } + + @Override + public IMqttsnMessage createSubscribe(int QoS, MqttsnConstants.TOPIC_TYPE type, int topicId) throws MqttsnCodecException { + + MqttsnWireUtils.validateQoS(QoS); + MqttsnWireUtils.validate16Bit(topicId); + + MqttsnSubscribe msg = new MqttsnSubscribe(); + msg.setQoS(QoS); + switch (type) { + case NORMAL: + msg.setNormalTopicAlias(topicId); + break; + case PREDEFINED: + msg.setPredefinedTopicAlias(topicId); + break; +// case SHORT: +// msg.setTopicName(topicId); +// break; + default: + throw new MqttsnCodecException("subscribe method only supports predefined and normal topic id types"); + } + return msg; + } + + @Override + public IMqttsnMessage createSubscribe(int QoS, String topicName) throws MqttsnCodecException { + + MqttsnWireUtils.validateQoS(QoS); + MqttsnSubscribe msg = new MqttsnSubscribe(); + msg.setQoS(QoS); + msg.setTopicName(topicName); + return msg; + } + + @Override + public IMqttsnMessage createSuback(int grantedQoS, int topicId, int returnCode) throws MqttsnCodecException { + + MqttsnWireUtils.validateQoS(grantedQoS); + MqttsnWireUtils.validate16Bit(topicId); + MqttsnWireUtils.validate8Bit(returnCode); + + MqttsnSuback msg = new MqttsnSuback(); + msg.setQoS(grantedQoS); + msg.setTopicId(topicId); + msg.setReturnCode(returnCode); + return msg; + } + + @Override + public IMqttsnMessage createUnsubscribe(MqttsnConstants.TOPIC_TYPE type, int topicId) throws MqttsnCodecException { + + MqttsnWireUtils.validate16Bit(topicId); + MqttsnUnsubscribe msg = new MqttsnUnsubscribe(); + switch (type) { + case NORMAL: + msg.setNormalTopicAlias(topicId); + break; + case PREDEFINED: + msg.setPredefinedTopicAlias(topicId); + break; + default: + throw new MqttsnCodecException("subscribe method only supports predefined and normal topic id types"); + } + return msg; + } + + @Override + public IMqttsnMessage createUnsubscribe(String topicName) throws MqttsnCodecException { + MqttsnUnsubscribe msg = new MqttsnUnsubscribe(); + msg.setTopicName(topicName); + return msg; + } + + @Override + public IMqttsnMessage createUnsuback() throws MqttsnCodecException { + MqttsnUnsuback msg = new MqttsnUnsuback(); + return msg; + } + + @Override + public IMqttsnMessage createPingreq(String clientId) throws MqttsnCodecException { + MqttsnPingreq msg = new MqttsnPingreq(); + msg.setClientId(clientId); + return msg; + } + + @Override + public IMqttsnMessage createPingresp() throws MqttsnCodecException { + MqttsnPingresp msg = new MqttsnPingresp(); + return msg; + } + + @Override + public IMqttsnMessage createDisconnect() throws MqttsnCodecException { + MqttsnDisconnect msg = new MqttsnDisconnect(); + return msg; + } + + @Override + public IMqttsnMessage createDisconnect(int duration) throws MqttsnCodecException { + + MqttsnWireUtils.validate16Bit(duration); + MqttsnDisconnect msg = new MqttsnDisconnect(); + msg.setDuration(duration); + return msg; + } + + @Override + public IMqttsnMessage createEncapsulatedMessage(String wirelessNodeId, int radius, byte[] messageData) throws MqttsnCodecException { + + MqttsnWireUtils.validate8Bit(radius); + MqttsnEncapsmsg msg = new MqttsnEncapsmsg(); + msg.setEncapsulatedMsg(messageData); + msg.setRadius(radius); + msg.setWirelessNodeId(wirelessNodeId); + return msg; + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnMessage.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnMessage.java new file mode 100644 index 00000000..3a6eff38 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnMessage.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; +import org.slj.mqtt.sn.spi.IMqttsnMessage; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; +import org.slj.mqtt.sn.wire.version1_2.Mqttsn_v1_2_Codec; + +public abstract class AbstractMqttsnMessage implements IMqttsnMessage { + + protected final int messageType; + + /* The MsgId field is 2-octet long and corresponds to the MQTT ‘Message ID’ parameter. It allows the sender to + match a message with its corresponding acknowledgment. */ + protected int msgId; + + protected int returnCode; + + public AbstractMqttsnMessage() { + messageType = getMessageType(); + } + + public int getMsgId() { + return msgId; + } + + public void setMsgId(int msgId) { + if (!needsMsgId()) throw new MqttsnCodecException("unable to set msg id on message type"); + this.msgId = msgId; + } + + public int getReturnCode() { + return returnCode; + } + + public void setReturnCode(int returnCode) { + this.returnCode = returnCode; + } + + public boolean isErrorMessage() { + return returnCode != MqttsnConstants.RETURN_CODE_ACCEPTED; + } + + /** + * Reads the remaining body from the data allowing for a header size defined in its smallest + * form for convenience but adjusted if the data is an extended type ie. > 255 bytes + */ + protected byte[] readRemainingBytesFromIndexAdjusted(byte[] data, int headerSize) { + int offset = (Mqttsn_v1_2_Codec.isExtendedMessage(data) ? headerSize + 2 : headerSize); + return readBytesFromIndexAdjusted(data, headerSize, data.length - offset); + } + + /** + * Reads the number of bytes from the data allowing for a header size defined in its smallest + * form for convenience but adjusted if the data is an extended type ie. > 255 bytes + */ + protected byte[] readBytesFromIndexAdjusted(byte[] data, int headerSize, int length) { + int size = Mqttsn_v1_2_Codec.readMessageLength(data); + int offset = (Mqttsn_v1_2_Codec.isExtendedMessage(data) ? headerSize + 2 : headerSize); + byte[] msgData = new byte[length]; + System.arraycopy(data, offset, msgData, 0, length); + return msgData; + } + + /** + * Reads an 8 bit field from the array starting at the given index (which will be auto-adjusted if the + * data is an extended type ie. > 255 bytes) + */ + protected int read8BitAdjusted(byte[] data, int startIdx) { + return MqttsnWireUtils.read8bit(Mqttsn_v1_2_Codec.readHeaderByteWithOffset(data, startIdx)); + } + + /** + * Reads a 16 bit field from the array starting at the given index (which will be auto-adjusted if the + * data is an extended type ie. > 255 bytes) + */ + protected int read16BitAdjusted(byte[] data, int startIdx) { + return MqttsnWireUtils.read16bit( + Mqttsn_v1_2_Codec.readHeaderByteWithOffset(data, startIdx), + Mqttsn_v1_2_Codec.readHeaderByteWithOffset(data, startIdx + 1)); + } + + public String getMessageName() { + return getClass().getSimpleName(); + } + + public boolean needsMsgId() { + return false; + } + + public abstract int getMessageType(); + + public abstract void decode(byte[] data) throws MqttsnCodecException; + + public abstract byte[] encode() throws MqttsnCodecException; + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(getMessageName()); + if (needsMsgId()) { + sb.append('{').append(getMsgId()).append("}"); + } + return sb.toString(); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnMessageWithFlagsField.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnMessageWithFlagsField.java new file mode 100644 index 00000000..01c12386 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnMessageWithFlagsField.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; + + +public abstract class AbstractMqttsnMessageWithFlagsField extends AbstractMqttsnMessage { + + /* set to “0” if message is sent for the first time; set to “1” if retransmitted (only relevant within PUBLISH messages); */ + boolean dupRedelivery = false; + + /* meaning as with MQTT for QoS level 0, 1, and 2; set to “0b00” for QoS level 0, + “0b01” for QoS level1, “0b10” for QoS level 2, and “0b11” for new QoS level -1 */ + int QoS; + + /* same meaning as with MQTT (only relevant within PUBLISH messages); */ + boolean retainedPublish = false; + + /* if set, indicates that client is asking for Will topic and Will message prompting + (only relevant within CONNECT message) */ + boolean will = false; + + /* same meaning as with MQTT, however extended for Will topic and Will message + (only relevant within CONNECT message); */ + boolean cleanSession = false; + + /* indicates whether the field TopicId or TopicName included in this message contains a normal topic id + (set to “0b00”), a pre-defined topic id (set to “0b01”), or a short topic name (set to “0b10”). + The value “0b11” is reserved. */ + int topicType; + + public boolean isDupRedelivery() { + return dupRedelivery; + } + + public void setDupRedelivery(boolean dupRedelivery) { + this.dupRedelivery = dupRedelivery; + } + + public int getQoS() { + return QoS == 3 ? -1 : QoS; + } + + public void setQoS(int qoS) { + if (qoS != MqttsnConstants.QoSM1 && + qoS != MqttsnConstants.QoS0 && + qoS != MqttsnConstants.QoS1 && + qoS != MqttsnConstants.QoS2) { + throw new IllegalArgumentException("unable to set invalid QoS value on message " + qoS); + } + QoS = qoS; + } + + public boolean isRetainedPublish() { + return retainedPublish; + } + + public void setRetainedPublish(boolean retainedPublish) { + this.retainedPublish = retainedPublish; + } + + public boolean isWill() { + return will; + } + + public void setWill(boolean will) { + this.will = will; + } + + public boolean isCleanSession() { + return cleanSession; + } + + public void setCleanSession(boolean cleanSession) { + this.cleanSession = cleanSession; + } + + public int getTopicType() { + return topicType; + } + + protected void setTopicType(byte topicType) { + if (topicType != MqttsnConstants.TOPIC_PREDEFINED && + topicType != MqttsnConstants.TOPIC_NORMAL && + topicType != MqttsnConstants.TOPIC_SHORT && + topicType != MqttsnConstants.TOPIC_RESERVED) { + throw new IllegalArgumentException("unable to set invalid topicIdType value on message " + topicType); + } + this.topicType = topicType; + } + + protected void readFlags(byte v) { + /** + DUP QoS Retain Will CleanSession TopicIdType + (bit 7) (6,5) (4) (3) (2) (1,0) + **/ + + //error redelivery + dupRedelivery = ((v & 0x80) >> 7 != 0); + + //qos + QoS = (v & 0x60) >> 5; + + //retained publish + retainedPublish = ((v & 0x10) >> 4 != 0); + + //will + will = ((v & 0x08) >> 3 != 0); + + //clean session + cleanSession = ((v & 0x04) >> 2 != 0); + + //topic type + topicType = (v & 0x03); + } + + protected byte writeFlags() { + /** + DUP QoS Retain Will CleanSession TopicIdType + (bit 7) (6,5) (4) (3) (2) (1,0) + **/ + + byte v = 0x00; + + //dup redelivery + if (dupRedelivery) v |= 0x80; + + //qos + if (QoS == MqttsnConstants.QoS1) v |= 0x20; + else if (QoS == MqttsnConstants.QoS2) v |= 0x40; + else if (QoS == MqttsnConstants.QoSM1) v |= 0x60; + + //retained publish + if (retainedPublish) v |= 0x10; + + //will message + if (will) v |= 0x08; + + //is clean session + if (cleanSession) v |= 0x04; + + //topic type + if (topicType == MqttsnConstants.TOPIC_PREDEFINED) v |= 0x01; + else if (topicType == MqttsnConstants.TOPIC_SHORT) v |= 0x02; + + return v; + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnMessageWithTopicData.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnMessageWithTopicData.java new file mode 100644 index 00000000..b447ad77 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnMessageWithTopicData.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; + +public abstract class AbstractMqttsnMessageWithTopicData extends AbstractMqttsnMessageWithFlagsField { + + protected byte[] topicData; + + public String getTopicName() { + if (topicType == MqttsnConstants.TOPIC_PREDEFINED) + throw new IllegalStateException("unable to parse string data from predefined topic alias"); + return new String(topicData, MqttsnConstants.CHARSET); + } + + public void setTopicName(String topicName) { + setTopicType(topicName != null && topicName.length() <= 2 ? MqttsnConstants.TOPIC_SHORT : MqttsnConstants.TOPIC_NORMAL); + topicData = topicName.getBytes(MqttsnConstants.CHARSET); + } + + public void setPredefinedTopicAlias(int topicAlias) { + setTopicType(MqttsnConstants.TOPIC_PREDEFINED); + setTopicAliasId(topicAlias); + } + + public void setNormalTopicAlias(int topicAlias) { + setTopicType(MqttsnConstants.TOPIC_NORMAL); + setTopicAliasId(topicAlias); + } + + public int readTopicDataAsInteger() { + return MqttsnWireUtils.read16bit(topicData[0], topicData[1]); + } + + public byte[] getTopicData() { + return topicData; + } + + protected void setTopicData(byte[] data) { + topicData = data; + } + + protected void setTopicAliasId(int topicAlias) { + topicData = new byte[2]; + topicData[0] = (byte) ((topicAlias >> 8) & 0xFF); + topicData[1] = (byte) (topicAlias & 0xFF); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnPublishMessageConfirmation.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnPublishMessageConfirmation.java new file mode 100644 index 00000000..84a3910f --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnPublishMessageConfirmation.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +public abstract class AbstractMqttsnPublishMessageConfirmation extends AbstractMqttsnMessage { + + public boolean needsMsgId() { + return true; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + msgId = read16BitAdjusted(data, 2); + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + byte[] data = new byte[4]; + data[0] = (byte) data.length; + data[1] = (byte) getMessageType(); + data[2] = (byte) ((msgId >> 8) & 0xFF); + data[3] = (byte) (msgId & 0xFF); + return data; + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnSimpleMessage.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnSimpleMessage.java new file mode 100644 index 00000000..65a9d457 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnSimpleMessage.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +public abstract class AbstractMqttsnSimpleMessage extends AbstractMqttsnMessage { + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + byte[] data = new byte[2]; + data[0] = (byte) data.length; + data[1] = (byte) getMessageType(); + return data; + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnSubscribeUnsubscribe.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnSubscribeUnsubscribe.java new file mode 100644 index 00000000..b7616371 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnSubscribeUnsubscribe.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.codec.MqttsnCodecException; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; +import org.slj.mqtt.sn.wire.version1_2.Mqttsn_v1_2_Codec; + +public abstract class AbstractMqttsnSubscribeUnsubscribe extends AbstractMqttsnMessageWithTopicData { + + public boolean needsMsgId() { + return true; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + readFlags(Mqttsn_v1_2_Codec.readHeaderByteWithOffset(data, 2)); + msgId = read16BitAdjusted(data, 3); + topicData = readRemainingBytesFromIndexAdjusted(data, 5); + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + byte[] msg; + int length = 5 + topicData.length; + int idx = 0; + + if ((length) > 0xFF) { + length += 2; + msg = new byte[length]; + msg[idx++] = (byte) 0x01; + msg[idx++] = ((byte) (0xFF & (length >> 8))); + msg[idx++] = ((byte) (0xFF & length)); + } else { + msg = new byte[length]; + msg[idx++] = (byte) length; + } + + msg[idx++] = (byte) getMessageType(); + msg[idx++] = writeFlags(); + + msg[idx++] = (byte) ((msgId >> 8) & 0xFF); + msg[idx++] = (byte) (msgId & 0xFF); + + if (topicData != null && topicData.length > 0) { + System.arraycopy(topicData, 0, msg, idx, topicData.length); + } + + return msg; + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnWillMessage.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnWillMessage.java new file mode 100644 index 00000000..afa68589 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnWillMessage.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +public abstract class AbstractMqttsnWillMessage extends AbstractMqttsnMessage { + + protected byte[] msgData; + + public byte[] getMsgData() { + return msgData; + } + + public void setMsgData(byte[] msgData) { + this.msgData = msgData; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + if (data.length > 2) { + msgData = readRemainingBytesFromIndexAdjusted(data, 2); + } + } + + @Override + public byte[] encode() throws MqttsnCodecException { + byte[] data = new byte[2 + (msgData == null ? 0 : msgData.length)]; + data[0] = (byte) data.length; + data[1] = (byte) getMessageType(); + + if (msgData != null && msgData.length > 0) { + System.arraycopy(msgData, 0, data, 2, msgData.length); + } + + return data; + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnWillTopicMessage.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnWillTopicMessage.java new file mode 100644 index 00000000..f9238c96 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnWillTopicMessage.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; +import org.slj.mqtt.sn.wire.version1_2.Mqttsn_v1_2_Codec; + +public abstract class AbstractMqttsnWillTopicMessage extends AbstractMqttsnMessageWithFlagsField { + + byte[] willTopicData; + + public String getWillTopicData() { + return willTopicData != null && willTopicData.length > 0 ? new String(willTopicData, MqttsnConstants.CHARSET) : null; + } + + public void setWillTopic(String willTopic) { + this.willTopicData = willTopic.getBytes(MqttsnConstants.CHARSET); + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + readFlags(Mqttsn_v1_2_Codec.readHeaderByteWithOffset(data, 2)); + willTopicData = readRemainingBytesFromIndexAdjusted(data, 3); + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + byte[] msg; + int length = 3 + willTopicData.length; + int idx = 0; + + if ((length) > 0xFF) { + length += 2; + msg = new byte[length]; + msg[idx++] = (byte) 0x01; + msg[idx++] = ((byte) (0xFF & (length >> 8))); + msg[idx++] = ((byte) (0xFF & length)); + } else { + msg = new byte[length]; + msg[idx++] = (byte) length; + } + + msg[idx++] = (byte) getMessageType(); + msg[idx++] = writeFlags(); + + if (willTopicData != null && willTopicData.length > 0) { + System.arraycopy(willTopicData, 0, msg, idx, willTopicData.length); + } + + return msg; + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnWillresp.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnWillresp.java new file mode 100644 index 00000000..2c8527a4 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/AbstractMqttsnWillresp.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +public abstract class AbstractMqttsnWillresp extends AbstractMqttsnMessage { + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + setReturnCode(read8BitAdjusted(data, 2)); + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + byte[] data = new byte[3]; + data[0] = (byte) data.length; + data[1] = (byte) getMessageType(); + data[2] = (byte) getReturnCode(); + return data; + } + +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnAdvertise.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnAdvertise.java new file mode 100644 index 00000000..0e02a3bd --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnAdvertise.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +public class MqttsnAdvertise extends AbstractMqttsnMessage { + + protected int gatewayId; + protected int duration; + + public int getGatewayId() { + return gatewayId; + } + + public void setGatewayId(int gatewayId) { + this.gatewayId = gatewayId; + } + + public int getDuration() { + return duration; + } + + public void setDuration(int duration) { + this.duration = duration; + } + + @Override + public int getMessageType() { + return MqttsnConstants.ADVERTISE; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + gatewayId = read8BitAdjusted(data, 2); + duration = read16BitAdjusted(data, 3); + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + byte[] data = new byte[5]; + data[0] = (byte) data.length; + data[1] = (byte) getMessageType(); + data[2] = (byte) gatewayId; + data[3] = (byte) ((duration >> 8) & 0xFF); + data[4] = (byte) (duration & 0xFF); + return data; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnAdvertise{"); + sb.append("gatewayId=").append(gatewayId); + sb.append(", duration=").append(duration); + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnConnack.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnConnack.java new file mode 100644 index 00000000..6adbc275 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnConnack.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +public class MqttsnConnack extends AbstractMqttsnMessage { + + @Override + public int getMessageType() { + return MqttsnConstants.CONNACK; + } + + @Override + public void decode(byte[] arr) throws MqttsnCodecException { + setReturnCode(arr[arr.length - 1]); + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + byte[] msg = new byte[3]; + msg[0] = 3; + msg[1] = (byte) getMessageType(); + msg[2] = (byte) getReturnCode(); + return msg; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnConnack{"); + sb.append("messageType=").append(messageType); + sb.append(", returnCode=").append(returnCode); + sb.append('}'); + return sb.toString(); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnConnect.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnConnect.java new file mode 100644 index 00000000..b2587db1 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnConnect.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; +import org.slj.mqtt.sn.spi.IMqttsnIdentificationPacket; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; +import org.slj.mqtt.sn.wire.version1_2.Mqttsn_v1_2_Codec; + +/** + * NB: despite the spec only allowing 23 chars in the clientId field, this type has been designed safely to support + * clientIds which take the message into an extended type (> 255). + */ +public class MqttsnConnect extends AbstractMqttsnMessageWithFlagsField implements IMqttsnIdentificationPacket { + + /* The Duration field is 2-octet long and specifies the duration of a time period in seconds. + The maximum value that can be encoded is approximately 18 hours. */ + protected int duration; + + protected int protocolId; + + /* 1-23 characters long string that uniquely identifies the client to the server */ + protected String clientId = null; + + public int getDuration() { + return duration; + } + + public void setDuration(int duration) { + this.duration = duration; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public int getProtocolId() { + return protocolId; + } + + public void setProtocolId(int protocolId) { + this.protocolId = protocolId; + } + + @Override + public int getMessageType() { + return MqttsnConstants.CONNECT; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + + if (Mqttsn_v1_2_Codec.isExtendedMessage(data)) { + readFlags(data[4]); + } else { + readFlags(data[2]); + } + + protocolId = read8BitAdjusted(data, 3); + duration = read16BitAdjusted(data, 4); + + byte[] body = readRemainingBytesFromIndexAdjusted(data, 6); + if (body.length > 0) { + clientId = new String(body, MqttsnConstants.CHARSET); + } + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + int length = 6 + (clientId == null ? 0 : clientId.length()); + byte[] msg = null; + int idx = 0; + if ((length) > 0xFF) { + length += 2; + msg = new byte[length]; + msg[idx++] = (byte) 0x01; + msg[idx++] = ((byte) (0xFF & (length >> 8))); + msg[idx++] = ((byte) (0xFF & length)); + } else { + msg = new byte[length]; + msg[idx++] = (byte) length; + } + + msg[idx++] = (byte) getMessageType(); + msg[idx++] = writeFlags(); + msg[idx++] = (byte) protocolId; //protocol id + + msg[idx++] = (byte) ((duration >> 8) & 0xFF); + msg[idx++] = (byte) (duration & 0xFF); + + if (clientId != null) { + byte[] clientIdArr = clientId.getBytes(MqttsnConstants.CHARSET); + System.arraycopy(clientIdArr, 0, msg, idx, clientIdArr.length); + } + + return msg; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnConnect{"); + sb.append("duration=").append(duration); + sb.append(", protocolId=").append(protocolId); + sb.append(", clientId='").append(clientId).append('\''); + sb.append(", will=").append(will); + sb.append(", cleanSession=").append(cleanSession); + sb.append('}'); + return sb.toString(); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnDisconnect.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnDisconnect.java new file mode 100644 index 00000000..3fb06a0d --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnDisconnect.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +public class MqttsnDisconnect extends AbstractMqttsnMessage { + + protected int duration; + + public int getDuration() { + return duration; + } + + public void setDuration(int duration) { + this.duration = duration; + } + + @Override + public int getMessageType() { + return MqttsnConstants.DISCONNECT; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + if (data.length == 4) { + duration = read16BitAdjusted(data, 2); + } + } + + @Override + public byte[] encode() throws MqttsnCodecException { + byte[] data = new byte[duration > 0 ? 4 : 2]; + data[0] = (byte) data.length; + data[1] = (byte) getMessageType(); + if (data.length == 4) { + data[2] = (byte) ((duration >> 8) & 0xFF); + data[3] = (byte) (duration & 0xFF); + } + return data; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnDisconnect{"); + sb.append("duration=").append(duration); + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnEncapsmsg.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnEncapsmsg.java new file mode 100644 index 00000000..202581e3 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnEncapsmsg.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; +import org.slj.mqtt.sn.wire.version1_2.Mqttsn_v1_2_Codec; + +import java.util.Arrays; + +public class MqttsnEncapsmsg extends AbstractMqttsnMessage { + + protected byte[] encapsulatedMsg; + protected byte[] wirelessNodeId; + int radius; + + public byte[] getEncapsulatedMsg() { + return encapsulatedMsg; + } + + public void setEncapsulatedMsg(byte[] encapsulatedMsg) { + this.encapsulatedMsg = encapsulatedMsg; + } + + public String getWirelessNodeId() { + return wirelessNodeId != null && wirelessNodeId.length > 0 ? + new String(wirelessNodeId, MqttsnConstants.CHARSET) : null; + } + + public void setWirelessNodeId(String wirelessNodeIdStr) { + this.wirelessNodeId = + wirelessNodeIdStr == null ? null : wirelessNodeIdStr.getBytes(MqttsnConstants.CHARSET); + } + + public int getRadius() { + return radius; + } + + public void setRadius(int radius) { + this.radius = radius; + } + + @Override + public int getMessageType() { + return MqttsnConstants.ENCAPSMSG; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + int length = Mqttsn_v1_2_Codec.readMessageLength(data); + + int offset = 0; + if (Mqttsn_v1_2_Codec.isExtendedMessage(data)) { + offset = 2; + } + int idx = 2 + offset; + byte ctrl = data[idx]; + radius = ctrl & 0x03; + + //idx = 4 | 2 + wirelessNodeId = new byte[length - idx]; + if (wirelessNodeId.length > 0) { + System.arraycopy(data, idx, wirelessNodeId, 0, wirelessNodeId.length); + idx += wirelessNodeId.length; + } + + //-- the rest is the next message + //idx = 4 | 2 + encapsulatedMsg = new byte[data.length - length]; + if (encapsulatedMsg.length > 0) { + System.arraycopy(data, idx, encapsulatedMsg, 0, encapsulatedMsg.length); + } + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + + //Length: 1-octet long, specifies the number of octets up to the end of the "Wireless Node Id" field (incl. the Length octet itself) + int length = 3 + + (wirelessNodeId != null ? wirelessNodeId.length : 0); + byte[] msg; + int idx = 0; + + if ((length) > 0xFF) { + length += 2; + msg = new byte[length]; + msg[idx++] = (byte) 0x01; + msg[idx++] = ((byte) (0xFF & (length >> 8))); + msg[idx++] = ((byte) (0xFF & length)); + } else { + msg = new byte[length]; + msg[idx++] = (byte) length; + } + + msg[idx++] = (byte) getMessageType(); + + //CTRL +// reserved (bit 7:2) + byte ctrl = 0; + ctrl |= radius; //(bit 1,0) + msg[idx++] = ctrl; + + byte[] nodeId; + if (wirelessNodeId != null && wirelessNodeId.length > 0) { + System.arraycopy(wirelessNodeId, 0, msg, idx, wirelessNodeId.length); + idx += wirelessNodeId.length; + } + + if (encapsulatedMsg != null && encapsulatedMsg.length > 0) { + System.arraycopy(encapsulatedMsg, 0, msg, idx, encapsulatedMsg.length); + } + + return msg; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnEncapsmsg{"); + sb.append("wirelessNodeId=").append(Arrays.toString(wirelessNodeId)); + sb.append(", radius=").append(radius); + sb.append('}'); + return sb.toString(); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnGwInfo.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnGwInfo.java new file mode 100644 index 00000000..d3825bc8 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnGwInfo.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +public class MqttsnGwInfo extends AbstractMqttsnMessage { + + protected int gatewayId; + protected String gatewayAddress; + + public int getGatewayId() { + return gatewayId; + } + + public void setGatewayId(int gatewayId) { + this.gatewayId = gatewayId; + } + + public String getGatewayAddress() { + return gatewayAddress; + } + + public void setGatewayAddress(String gatewayAddress) { + this.gatewayAddress = gatewayAddress; + } + + @Override + public int getMessageType() { + return MqttsnConstants.GWINFO; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + + gatewayId = (data[2] & 0xFF); + byte[] body = new byte[data[0] - 3]; + if (body.length > 0) { + System.arraycopy(data, 3, body, 0, body.length); + gatewayAddress = new String(body, MqttsnConstants.CHARSET); + } + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + int length = 3 + (gatewayAddress == null ? 0 : gatewayAddress.length()); + byte[] data = new byte[length]; + data[0] = (byte) length; + data[1] = (byte) getMessageType(); + data[2] = (byte) gatewayId; + if (gatewayAddress != null) { + System.arraycopy(gatewayAddress.getBytes(MqttsnConstants.CHARSET), 0, data, 3, gatewayAddress.length()); + } + return data; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnGwInfo{"); + sb.append("gatewayId=").append(gatewayId); + sb.append(", gatewayAddress='").append(gatewayAddress).append('\''); + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPingreq.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPingreq.java new file mode 100644 index 00000000..f34ab260 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPingreq.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; +import org.slj.mqtt.sn.spi.IMqttsnIdentificationPacket; + +public class MqttsnPingreq extends AbstractMqttsnMessage implements IMqttsnIdentificationPacket { + + protected String clientId; + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + @Override + public int getMessageType() { + return MqttsnConstants.PINGREQ; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + byte[] body = readRemainingBytesFromIndexAdjusted(data, 2); + if (body.length > 0) { + clientId = new String(body, MqttsnConstants.CHARSET); + } + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + int length = 2 + (clientId == null ? 0 : clientId.length()); + byte[] msg = null; + int idx = 0; + if (length > 0xFF) { + length += 2; + msg = new byte[length]; + msg[idx++] = (byte) 0x01; + msg[idx++] = ((byte) (0xFF & (length >> 8))); + msg[idx++] = ((byte) (0xFF & length)); + } else { + msg = new byte[length]; + msg[idx++] = (byte) length; + } + + msg[idx++] = (byte) getMessageType(); + + if (clientId != null) { + System.arraycopy(clientId.getBytes(MqttsnConstants.CHARSET), 0, msg, idx, clientId.length()); + } + return msg; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnPingreq{"); + sb.append("clientId='").append(clientId).append('\''); + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPingresp.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPingresp.java new file mode 100644 index 00000000..eefc28cd --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPingresp.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +public class MqttsnPingresp extends AbstractMqttsnMessage { + + @Override + public int getMessageType() { + return MqttsnConstants.PINGRESP; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + byte[] data = new byte[2]; + data[0] = (byte) data.length; + data[1] = (byte) getMessageType(); + return data; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnPingresp{"); + sb.append('}'); + return sb.toString(); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPuback.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPuback.java new file mode 100644 index 00000000..d2b12f6f --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPuback.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +public class MqttsnPuback extends AbstractMqttsnMessage { + + protected int topicId; + + public boolean needsMsgId() { + return true; + } + + public int getTopicId() { + return topicId; + } + + public void setTopicId(int topicId) { + this.topicId = topicId; + } + + @Override + public int getMessageType() { + return MqttsnConstants.PUBACK; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + + topicId = read16BitAdjusted(data, 2); + msgId = read16BitAdjusted(data, 4); + returnCode = (data[6] & 0xFF); + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + byte[] data = new byte[7]; + data[0] = (byte) data.length; + data[1] = (byte) getMessageType(); + + data[2] = (byte) ((topicId >> 8) & 0xFF); + data[3] = (byte) (topicId & 0xFF); + + data[4] = (byte) ((msgId >> 8) & 0xFF); + data[5] = (byte) (msgId & 0xFF); + + data[6] = (byte) returnCode; + + return data; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnPuback{"); + sb.append("topicId=").append(topicId); + sb.append(", msgId=").append(msgId); + sb.append(", returnCode=").append(returnCode); + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPubcomp.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPubcomp.java new file mode 100644 index 00000000..54dd5e22 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPubcomp.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; + +public class MqttsnPubcomp extends AbstractMqttsnPublishMessageConfirmation { + + @Override + public int getMessageType() { + return MqttsnConstants.PUBCOMP; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnPubcomp{"); + sb.append("msgId=").append(msgId); + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPublish.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPublish.java new file mode 100644 index 00000000..ea150d32 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPublish.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; +import org.slj.mqtt.sn.wire.version1_2.Mqttsn_v1_2_Codec; + +import java.util.Arrays; + +public class MqttsnPublish extends AbstractMqttsnMessageWithTopicData { + + public boolean needsMsgId() { + return true; + } + + protected byte[] data; + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + } + + @Override + public int getMessageType() { + return MqttsnConstants.PUBLISH; + } + + @Override + public void decode(byte[] arr) throws MqttsnCodecException { + readFlags(Mqttsn_v1_2_Codec.readHeaderByteWithOffset(arr, 2)); + setTopicData(readBytesFromIndexAdjusted(arr, 3, 2)); + msgId = read16BitAdjusted(arr, 5); + data = readRemainingBytesFromIndexAdjusted(arr, 7); + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + byte[] msg; + int length = data.length + 7; + int idx = 0; + + if ((length) > 0xFF) { + length += 2; + msg = new byte[length]; + msg[idx++] = (byte) 0x01; + msg[idx++] = ((byte) (0xFF & (length >> 8))); + msg[idx++] = ((byte) (0xFF & length)); + } else { + msg = new byte[length]; + msg[idx++] = (byte) length; + } + + msg[idx++] = (byte) getMessageType(); + msg[idx++] = writeFlags(); + + //-- copy in the topic data + System.arraycopy(topicData, 0, msg, idx, topicData.length); + idx += topicData.length; + + msg[idx++] = (byte) ((msgId >> 8) & 0xFF); + msg[idx++] = (byte) (msgId & 0xFF); + + System.arraycopy(data, 0, msg, msg.length - (data.length), data.length); + return msg; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnPublish{"); + sb.append("topicData=").append(Arrays.toString(topicData)); + sb.append(", dup=").append(dupRedelivery); + sb.append(", QoS=").append(QoS); + sb.append(", retain=").append(retainedPublish); + sb.append(", topicIdType=").append(topicType); + sb.append(", msgId=").append(msgId); + sb.append('}'); + return sb.toString(); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPubrec.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPubrec.java new file mode 100644 index 00000000..ccd6222f --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPubrec.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; + +public class MqttsnPubrec extends AbstractMqttsnPublishMessageConfirmation { + + @Override + public int getMessageType() { + return MqttsnConstants.PUBREC; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnPubrec{"); + sb.append("msgId=").append(msgId); + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPubrel.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPubrel.java new file mode 100644 index 00000000..5d8879ad --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnPubrel.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; + +public class MqttsnPubrel extends AbstractMqttsnPublishMessageConfirmation { + + @Override + public int getMessageType() { + return MqttsnConstants.PUBREL; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnPubrel{"); + sb.append("msgId=").append(msgId); + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnRegack.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnRegack.java new file mode 100644 index 00000000..3bd519a9 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnRegack.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +public class MqttsnRegack extends AbstractMqttsnMessage { + + protected int topicId; + + public boolean needsMsgId() { + return true; + } + + public int getTopicId() { + return topicId; + } + + public void setTopicId(int topicId) { + this.topicId = topicId; + } + + @Override + public int getMessageType() { + return MqttsnConstants.REGACK; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + + topicId = read16BitAdjusted(data, 2); + msgId = read16BitAdjusted(data, 4); + returnCode = (data[6] & 0xFF); + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + byte[] data = new byte[7]; + data[0] = (byte) data.length; + data[1] = (byte) getMessageType(); + + data[2] = (byte) ((topicId >> 8) & 0xFF); + data[3] = (byte) (topicId & 0xFF); + + data[4] = (byte) ((msgId >> 8) & 0xFF); + data[5] = (byte) (msgId & 0xFF); + + data[6] = (byte) returnCode; + + return data; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnRegack{"); + sb.append("topicId=").append(topicId); + sb.append(", msgId=").append(msgId); + sb.append(", returnCode=").append(returnCode); + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnRegister.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnRegister.java new file mode 100644 index 00000000..9bb70674 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnRegister.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; +import org.slj.mqtt.sn.wire.version1_2.Mqttsn_v1_2_Codec; + +public class MqttsnRegister extends AbstractMqttsnMessage { + + protected int topicId; + protected String topicName; + + public boolean needsMsgId() { + return true; + } + + public int getTopicId() { + return topicId; + } + + public void setTopicId(int topicId) { + this.topicId = topicId; + } + + public String getTopicName() { + return topicName; + } + + public void setTopicName(String topicName) { + this.topicName = topicName; + } + + @Override + public int getMessageType() { + return MqttsnConstants.REGISTER; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + + if (Mqttsn_v1_2_Codec.isExtendedMessage(data)) { + topicId = read16BitAdjusted(data, 4); + msgId = read16BitAdjusted(data, 6); + } else { + topicId = read16BitAdjusted(data, 2); + msgId = read16BitAdjusted(data, 4); + } + + byte[] body = readRemainingBytesFromIndexAdjusted(data, 6); + if (body.length > 0) { + topicName = new String(body, MqttsnConstants.CHARSET); + } + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + byte[] topicByteArr = null; + int length = 6 + (topicName == null ? 0 : ((topicByteArr = topicName.getBytes(MqttsnConstants.CHARSET)).length)); + byte[] msg = null; + int idx = 0; + if ((length) > 0xFF) { + + length += 2; + + msg = new byte[length]; + msg[idx++] = (byte) 0x01; + msg[idx++] = ((byte) (0xFF & (length >> 8))); + msg[idx++] = ((byte) (0xFF & length)); + + } else { + + msg = new byte[length]; + msg[idx++] = (byte) length; + } + + msg[idx++] = (byte) getMessageType(); + + msg[idx++] = ((byte) (0xFF & (topicId >> 8))); + msg[idx++] = ((byte) (0xFF & topicId)); + + msg[idx++] = ((byte) (0xFF & (msgId >> 8))); + msg[idx++] = ((byte) (0xFF & msgId)); + + if (topicByteArr != null && topicByteArr.length > 0) { + System.arraycopy(topicByteArr, 0, msg, idx, topicByteArr.length); + } + + return msg; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnRegister{"); + sb.append("topicId=").append(topicId); + sb.append(", topicName='").append(topicName).append('\''); + sb.append(", msgId=").append(msgId); + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnSearchGw.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnSearchGw.java new file mode 100644 index 00000000..62ef1420 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnSearchGw.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +public class MqttsnSearchGw extends AbstractMqttsnMessage { + + protected int radius; + + public int getRadius() { + return radius; + } + + public void setRadius(int radius) { + this.radius = radius; + } + + @Override + public int getMessageType() { + return MqttsnConstants.SEARCHGW; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + radius = (data[2] & 0xFF); + } + + @Override + public byte[] encode() throws MqttsnCodecException { + int length = 3; + byte[] data = new byte[length]; + data[0] = (byte) length; + data[1] = (byte) getMessageType(); + data[2] = (byte) radius; + return data; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnSearchGw{"); + sb.append("radius=").append(radius); + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnSuback.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnSuback.java new file mode 100644 index 00000000..44a6e91f --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnSuback.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +public class MqttsnSuback extends AbstractMqttsnMessageWithFlagsField { + + protected int topicId; + + public boolean needsMsgId() { + return true; + } + + public int getTopicId() { + return topicId; + } + + public void setTopicId(int topicId) { + this.topicId = topicId; + } + + @Override + public int getMessageType() { + return MqttsnConstants.SUBACK; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + + readFlags(data[2]); + topicId = read16BitAdjusted(data, 3); + msgId = read16BitAdjusted(data, 5); + returnCode = read8BitAdjusted(data, 7); + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + byte[] data = new byte[8]; + data[0] = (byte) data.length; + data[1] = (byte) getMessageType(); + + data[2] = writeFlags(); + + data[3] = (byte) ((topicId >> 8) & 0xFF); + data[4] = (byte) (topicId & 0xFF); + + data[5] = (byte) ((msgId >> 8) & 0xFF); + data[6] = (byte) (msgId & 0xFF); + + data[7] = (byte) returnCode; + + return data; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnSuback{"); + sb.append("topicId=").append(topicId); + sb.append(", grantedQoS=").append(getQoS()); + sb.append(", msgId=").append(msgId); + sb.append(", returnCode=").append(returnCode); + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnSubscribe.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnSubscribe.java new file mode 100644 index 00000000..bdcb1b7d --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnSubscribe.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; + +import java.util.Arrays; + +public class MqttsnSubscribe extends AbstractMqttsnSubscribeUnsubscribe { + + @Override + public int getMessageType() { + return MqttsnConstants.SUBSCRIBE; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnSubscribe{"); + sb.append("topicData=").append(Arrays.toString(topicData)); + sb.append(", QoS=").append(QoS); + sb.append(", topicIdType=").append(topicType); + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnUnsuback.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnUnsuback.java new file mode 100644 index 00000000..20e492d5 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnUnsuback.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; + +public class MqttsnUnsuback extends AbstractMqttsnMessage { + + public boolean needsMsgId() { + return true; + } + + @Override + public int getMessageType() { + return MqttsnConstants.UNSUBACK; + } + + @Override + public void decode(byte[] data) throws MqttsnCodecException { + msgId = read16BitAdjusted(data, 2); + } + + @Override + public byte[] encode() throws MqttsnCodecException { + + byte[] data = new byte[4]; + data[0] = (byte) data.length; + data[1] = (byte) getMessageType(); + + data[2] = (byte) ((msgId >> 8) & 0xFF); + data[3] = (byte) (msgId & 0xFF); + + return data; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnUnsuback{"); + sb.append("msgId=").append(msgId); + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnUnsubscribe.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnUnsubscribe.java new file mode 100644 index 00000000..137f324c --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnUnsubscribe.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; + +import java.util.Arrays; + +public class MqttsnUnsubscribe extends AbstractMqttsnSubscribeUnsubscribe { + @Override + public int getMessageType() { + return MqttsnConstants.UNSUBSCRIBE; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnUnsubscribe{"); + sb.append("topicData=").append(Arrays.toString(topicData)); + sb.append(", topicIdType=").append(topicType); + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWillmsg.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWillmsg.java new file mode 100644 index 00000000..fd07785d --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWillmsg.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; + +public class MqttsnWillmsg extends AbstractMqttsnWillMessage { + + @Override + public int getMessageType() { + return MqttsnConstants.WILLMSG; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnWillmsg{"); + sb.append('}'); + return sb.toString(); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWillmsgreq.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWillmsgreq.java new file mode 100644 index 00000000..675e2b71 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWillmsgreq.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; + +public class MqttsnWillmsgreq extends AbstractMqttsnSimpleMessage { + + @Override + public int getMessageType() { + return MqttsnConstants.WILLMSGREQ; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnWillmsgreq{"); + sb.append('}'); + return sb.toString(); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWillmsgresp.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWillmsgresp.java new file mode 100644 index 00000000..ead36f25 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWillmsgresp.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; + +public class MqttsnWillmsgresp extends AbstractMqttsnWillresp { + + @Override + public int getMessageType() { + return MqttsnConstants.WILLMSGRESP; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnWillmsgresp{"); + sb.append('}'); + return sb.toString(); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWillmsgupd.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWillmsgupd.java new file mode 100644 index 00000000..5d5ff979 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWillmsgupd.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; + +public class MqttsnWillmsgupd extends AbstractMqttsnWillMessage { + + @Override + public int getMessageType() { + return MqttsnConstants.WILLMSGUPD; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnWillmsgupd{"); + sb.append('}'); + return sb.toString(); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWilltopic.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWilltopic.java new file mode 100644 index 00000000..255616c3 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWilltopic.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; + +import java.util.Arrays; + +public class MqttsnWilltopic extends AbstractMqttsnWillTopicMessage { + + @Override + public int getMessageType() { + return MqttsnConstants.WILLTOPIC; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnWilltopic{"); + sb.append("willTopicData=").append(Arrays.toString(willTopicData)); + sb.append('}'); + return sb.toString(); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWilltopicreq.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWilltopicreq.java new file mode 100644 index 00000000..79b54d8d --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWilltopicreq.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; + +public class MqttsnWilltopicreq extends AbstractMqttsnSimpleMessage { + + @Override + public int getMessageType() { + return MqttsnConstants.WILLTOPICREQ; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnWilltopicreq{"); + sb.append('}'); + return sb.toString(); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWilltopicresp.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWilltopicresp.java new file mode 100644 index 00000000..46fe7d76 --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWilltopicresp.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; + +public class MqttsnWilltopicresp extends AbstractMqttsnWillresp { + + @Override + public int getMessageType() { + return MqttsnConstants.WILLTOPICRESP; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnWilltopicresp{"); + sb.append('}'); + return sb.toString(); + } +} diff --git a/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWilltopicudp.java b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWilltopicudp.java new file mode 100644 index 00000000..01cbf3dd --- /dev/null +++ b/mqtt-sn-codec/src/main/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWilltopicudp.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.slj.mqtt.sn.MqttsnConstants; + +import java.util.Arrays; + +public class MqttsnWilltopicudp extends AbstractMqttsnWillTopicMessage { + + @Override + public int getMessageType() { + return MqttsnConstants.WILLTOPICUPD; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttsnWilltopicudp{"); + sb.append("willTopicData=").append(Arrays.toString(willTopicData)); + sb.append(", topicIdType=").append(topicType); + sb.append('}'); + return sb.toString(); + } +} diff --git a/mqtt-sn-codec/src/test/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWireTests.java b/mqtt-sn-codec/src/test/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWireTests.java new file mode 100644 index 00000000..d3be8093 --- /dev/null +++ b/mqtt-sn-codec/src/test/java/org/slj/mqtt/sn/wire/version1_2/payload/MqttsnWireTests.java @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.wire.version1_2.payload; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; +import org.slj.mqtt.sn.codec.MqttsnCodecs; +import org.slj.mqtt.sn.spi.IMqttsnCodec; +import org.slj.mqtt.sn.spi.IMqttsnMessage; +import org.slj.mqtt.sn.spi.IMqttsnMessageFactory; + +import java.util.Arrays; + +import static org.junit.Assert.*; + +public class MqttsnWireTests { + + static final byte _payload = 0x10; + static final String _path = "/topic/path"; + static final String _clientid = "client-id"; + static final int _radius = 2; + static final int _alias = 12; + static final int _qos = 2; + static final int _msgId = 254; +// static final int _msgId = MqttsnConstants.USIGNED_MAX_16; + + IMqttsnCodec codec; + IMqttsnMessageFactory factory; + + @Before + public void setup(){ + codec = MqttsnCodecs.MQTTSN_CODEC_VERSION_1_2; + factory = codec.createMessageFactory(); + } + + @Test + public void testMqttsnAdvertise() throws MqttsnCodecException { + IMqttsnMessage message = factory.createAdvertise(MqttsnConstants.USIGNED_MAX_8, + MqttsnConstants.USIGNED_MAX_16); + testWireMessage(message); + } + + @Test + public void testMqttsnConnack() throws MqttsnCodecException { + IMqttsnMessage message = factory.createConnack(MqttsnConstants.RETURN_CODE_ACCEPTED); + testWireMessage(message); + } + + @Test + public void testMqttsnConnect() throws MqttsnCodecException { + + //-- test normal length clientId + IMqttsnMessage message = factory.createConnect(_clientid, MqttsnConstants.USIGNED_MAX_16, false, true); + testWireMessage(message); + } + + @Test + public void testMqttsnConnectLongClientId() throws MqttsnCodecException { + + //-- test very long clientId + StringBuilder sb = new StringBuilder(1024); + for (int i = 0; i < 1024; i++){ + sb.append("A"); + } + + IMqttsnMessage message = factory.createConnect(sb.toString(), MqttsnConstants.USIGNED_MAX_16, false, true); + testWireMessage(message); + } + + @Test + public void testMqttsnDisconnect() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createDisconnect(); + testWireMessage(message); + } + + @Test + public void testMqttsnDisconnectWithDuration() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createDisconnect(MqttsnConstants.USIGNED_MAX_16); + testWireMessage(message); + } + + @Test + public void testMqttsnGwinfo() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createGwinfo(MqttsnConstants.USIGNED_MAX_8, "123:123123:0:c:12:2"); + testWireMessage(message); + } + + @Test + public void testMqttsnPingreq() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createPingreq(_clientid); + testWireMessage(message); + } + + @Test + public void testMqttsnPingresp() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createPingresp(); + testWireMessage(message); + } + + @Test + public void testMqttsnPubackError() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createPuback(_alias, MqttsnConstants.RETURN_CODE_INVALID_TOPIC_ID); + testWireMessage(message); + } + + @Test + public void testMqttsnPublishNormalTopic() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createPublish(_qos, true, false, MqttsnConstants.TOPIC_TYPE.NORMAL, _alias, payload(4)); + message.setMsgId(25); + testWireMessage(message); + } + + @Test + public void testMqttsnPublishPredefinedTopic() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createPublish(_qos, true, false, MqttsnConstants.TOPIC_TYPE.PREDEFINED, _alias, payload(4)); + testWireMessage(message); + } + + @Test + public void testMqttsnPublishShortTopic() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createPublish(_qos, true, false, "ab", payload(4)); + testWireMessage(message); + } + + @Test + public void testMqttsnPublishLong() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createPublish(_qos, true, false, MqttsnConstants.TOPIC_TYPE.PREDEFINED, _alias, payload(MqttsnConstants.USIGNED_MAX_16)); + testWireMessage(message); + } + + @Test + public void testMqttsnPubrel() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createPubrel(); + testWireMessage(message); + } + + @Test + public void testMqttsnPubrec() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createPubrec(); + testWireMessage(message); + } + + @Test + public void testMqttsnPuback() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createPuback(_alias, MqttsnConstants.RETURN_CODE_ACCEPTED); + testWireMessage(message); + } + + @Test + public void testMqttsnPubcomp() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createPubcomp(); + testWireMessage(message); + } + + @Test + public void testMqttsnRegack() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createRegack(_alias, MqttsnConstants.RETURN_CODE_ACCEPTED); + testWireMessage(message); + } + + @Test + public void testMqttsnRegackError() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createRegack(_alias, MqttsnConstants.RETURN_CODE_INVALID_TOPIC_ID); + testWireMessage(message); + } + + @Test + public void testMqttsnRegisterPath() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createRegister(_path); + testWireMessage(message); + } + + @Test + public void testMqttsnRegisterPathWithAlias() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createRegister(_alias, _path); + testWireMessage(message); + } + + @Test + public void testMqttsnSearchGw() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createSearchGw(_radius); + testWireMessage(message); + } + + @Test + public void testMqttsnSuback() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createSuback(_qos, _alias, MqttsnConstants.RETURN_CODE_ACCEPTED); + testWireMessage(message); + } + + @Test + public void testMqttsnSubscribePath() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createSubscribe(_qos, _path); + testWireMessage(message); + } + + @Test + public void testMqttsnSubscribePredefined() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createSubscribe(_qos, MqttsnConstants.TOPIC_TYPE.PREDEFINED, _alias); + testWireMessage(message); + } + + @Test + public void testMqttsnUnsubscribe() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createUnsubscribe(_path); + testWireMessage(message); + } + + @Test + public void testMqttsnUnsubscribePredefined() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createUnsubscribe(MqttsnConstants.TOPIC_TYPE.PREDEFINED, _alias); + testWireMessage(message); + } + + @Test + public void testMqttsnWillmsg() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createWillMsg(payload(50)); + testWireMessage(message); + } + + @Test + public void testMqttsnWillmsgreq() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createWillMsgReq(); + testWireMessage(message); + } + + @Test + public void testMqttsnWillmsgresp() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createWillMsgResp(MqttsnConstants.RETURN_CODE_ACCEPTED); + testWireMessage(message); + } + + @Test + public void testMqttsnWillmsgrespError() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createWillMsgResp(MqttsnConstants.RETURN_CODE_REJECTED_CONGESTION); + testWireMessage(message); + } + + @Test + public void testMqttsnWillmsgupd() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createWillMsgupd(payload(50)); + testWireMessage(message); + } + + @Test + public void testMqttsnWilltopic() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createWillTopic(_qos, true, _path); + testWireMessage(message); + } + + @Test + public void testMqttsnWilltopicreq() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createWillTopicReq(); + testWireMessage(message); + } + + @Test + public void testMqttsnWilltopicresp() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createWillTopicResp(MqttsnConstants.RETURN_CODE_ACCEPTED); + testWireMessage(message); + } + + @Test + public void testMqttsnWilltopicrespError() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createWillTopicResp(MqttsnConstants.RETURN_CODE_SERVER_UNAVAILABLE); + testWireMessage(message); + } + + @Test + public void testMqttsnWilltopicupd() throws MqttsnCodecException { + + IMqttsnMessage message = factory.createWillTopicupd(_qos, true, _path); + testWireMessage(message); + } + + protected void testWireMessage(IMqttsnMessage message) throws MqttsnCodecException { + + if(message.needsMsgId()){ + message.setMsgId(_msgId); + } + + String toString = message.toString(); + byte[] arr = codec.encode(message); + + System.out.println(String.format("before [%s] -> [%s]", toString, codec.print(message))); + + IMqttsnMessage decoded = codec.decode(arr); + String afterToString = decoded.toString(); + + System.out.println(String.format("after [%s] -> [%s]", afterToString, codec.print(message))); + + //-- first ensure the toStrings match since they contain the important data fields for each type + Assert.assertEquals("message content should match", toString, afterToString); + + //-- re-encode to ensure a full pass of all fields + byte[] reencoded = codec.encode(decoded); + Assert.assertArrayEquals("binary content should match", arr, reencoded); + } + + static byte[] payload(int size){ + + byte[] arr = new byte[size]; + Arrays.fill(arr, _payload); + return arr; + } +} \ No newline at end of file diff --git a/mqtt-sn-core/ext/create-keystore.sh b/mqtt-sn-core/ext/create-keystore.sh new file mode 100644 index 00000000..d13a118d --- /dev/null +++ b/mqtt-sn-core/ext/create-keystore.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# +# /* +# * Copyright (c) 2021 Simon Johnson +# * +# * Find me on GitHub: +# * https://github.com/simon622 +# * +# * Licensed to the Apache Software Foundation (ASF) under one +# * or more contributor license agreements. See the NOTICE file +# * distributed with this work for additional information +# * regarding copyright ownership. The ASF licenses this file +# * to you under the Apache License, Version 2.0 (the +# * "License"); you may not use this file except in compliance +# * with the License. You may obtain a copy of the License at +# * +# * http://www.apache.org/licenses/LICENSE-2.0 +# * +# * Unless required by applicable law or agreed to in writing, +# * software distributed under the License is distributed on an +# * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# * KIND, either express or implied. See the License for the +# * specific language governing permissions and limitations +# * under the License. +# */ +# +# + +rm my-keystore.jks 2> /dev/null +rm my-truststore.jks 2> /dev/null + +echo "=================================================" +echo "Generating keystore..." +echo "=================================================" + +keytool -genkeypair -alias a1 -dname cn=a1 \ + -validity 10000 -keyalg RSA -keysize 2048 \ + -keystore my-keystore.jks -keypass password -storepass password + +keytool -list -v -storepass password -keystore my-keystore.jks + +echo "=================================================" +echo "Generating truststore..." +echo "=================================================" + + keytool -exportcert -alias a1 \ + -keystore my-keystore.jks -keypass password -storepass password \ +| keytool -importcert -noprompt -alias a1 \ + -keystore my-truststore.jks -keypass password -storepass password + +keytool -list -v -storepass password -keystore my-truststore.jks \ No newline at end of file diff --git a/mqtt-sn-core/pom.xml b/mqtt-sn-core/pom.xml new file mode 100644 index 00000000..f0cf05bf --- /dev/null +++ b/mqtt-sn-core/pom.xml @@ -0,0 +1,66 @@ + + + + + 4.0.0 + + org.slj + mqtt-sn-core + 1.0.0 + + + UTF-8 + 4.13 + 3.8.1 + 1.8 + 1.8 + + + + + org.slj + mqtt-sn-codec + 1.0.0 + + + junit + junit + ${junit.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + \ No newline at end of file diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/cli/AbstractInteractiveCli.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/cli/AbstractInteractiveCli.java new file mode 100644 index 00000000..cfda5676 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/cli/AbstractInteractiveCli.java @@ -0,0 +1,402 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.cli; + +import org.slj.mqtt.sn.impl.AbstractMqttsnRuntime; +import org.slj.mqtt.sn.impl.AbstractMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.INetworkContext; +import org.slj.mqtt.sn.model.MqttsnOptions; +import org.slj.mqtt.sn.spi.IMqttsnMessage; +import org.slj.mqtt.sn.spi.IMqttsnTrafficListener; +import org.slj.mqtt.sn.spi.IMqttsnTransport; +import org.slj.mqtt.sn.spi.MqttsnException; + +import java.io.*; +import java.net.UnknownHostException; +import java.util.Date; +import java.util.Properties; +import java.util.Scanner; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Pattern; + +public abstract class AbstractInteractiveCli { + + static final String HOSTNAME = "hostName"; + static final String CLIENTID = "clientId"; + static final String PORT = "port"; + + protected final AtomicInteger sentByteCount = new AtomicInteger(0); + protected final AtomicInteger receiveByteCount = new AtomicInteger(0); + protected final AtomicInteger receiveCount = new AtomicInteger(0); + protected final AtomicInteger receivedPublishBytesCount = new AtomicInteger(0); + protected final AtomicInteger sentCount = new AtomicInteger(0); + protected final AtomicInteger publishedBytesCount = new AtomicInteger(0); + + protected boolean colors = false; + protected boolean useHistory = false; + protected String hostName; + protected int port; + protected String clientId; + + protected MqttsnOptions options; + protected AbstractMqttsnRuntimeRegistry runtimeRegistry; + protected AbstractMqttsnRuntime runtime; + + protected PrintStream output; + protected Scanner input; + protected volatile boolean closed = false; + + public void init(Scanner input, PrintStream output){ + this.input = input; + this.output = output; + colors = !System.getProperty("os.name").contains("Windows"); + } + + public void start() throws Exception { + if(input == null || output == null) throw new IllegalStateException("no init"); + message(String.format("Starting up interactive CLI with port=%s, clientId=%s", port, clientId)); + if(useHistory){ + saveConfig(); + } + + message(String.format("Creating runtime configuration.. DONE")); + options = createOptions(); + message(String.format("Creating runtime registry.. DONE")); + runtimeRegistry = createRuntimeRegistry(options, createTransport()); + message(String.format("Creating runtime .. DONE")); + runtime = createRuntime(runtimeRegistry, options); + + message("Adding runtime listeners.. DONE"); + runtime.registerReceivedListener((IMqttsnContext context, String topic, int qos, byte[] data) -> { + try { + receiveCount.incrementAndGet(); + receivedPublishBytesCount.addAndGet(data.length); + output.println(); + message(String.format(cli_blue() + "[<<<] PUBLISH [%s] bytes on [%s] from [%s] -> [%s]", data.length, topic, context.getId(), new String(data))); + } catch(Exception e){ + e.printStackTrace(); + } + }); + runtime.registerSentListener((IMqttsnContext context, UUID messageId, String topicName, int QoS, byte[] data) -> { + try { + sentCount.incrementAndGet(); + publishedBytesCount.addAndGet(data.length); + message(String.format(cli_blue() + "[>>>] PUBLISH [%s] bytes on [%s] to [%s] -> [%s]", data.length, topicName, context.getId(), new String(data))); + } catch(Exception e){ + e.printStackTrace(); + } + }); + + runtime.registerPublishFailedListener((IMqttsnContext context, UUID messageId, String topicName, int QoS, byte[] data, int retryCount) -> { + try { + sentCount.incrementAndGet(); + message(String.format(cli_blue() + "[XXX] PUBLISH failure (tried [%s] times) [%s] bytes on topic [%s], at QoS [%s] -> [%s]", retryCount, data.length, topicName, QoS, new String(data))); + } catch(Exception e){ + e.printStackTrace(); + } + }); + + message("Adding traffic listeners.. DONE"); + runtimeRegistry.withTrafficListener(new IMqttsnTrafficListener() { + @Override + public void trafficSent(INetworkContext context, byte[] data, IMqttsnMessage message) { + sentByteCount.addAndGet(data.length); + } + + @Override + public void trafficReceived(INetworkContext context, byte[] data, IMqttsnMessage message) { + receiveByteCount.addAndGet(data.length); + } + }); + + message("Runtime successfully initialised."); + } + + public void welcome(){ + output.println("==============================================="); + output.println("Welcome to " + getCLIName()); + output.println("==============================================="); + } + + public void exit(){ + output.println("==============================================="); + output.println("Goodbye from " + getCLIName()); + output.println("==============================================="); + } + + protected void configure() throws IOException { + + do{ + hostName = captureMandatoryString(input, output, "Please enter a valid host name or ip address"); + } while(!validHost()); + + port = captureMandatoryInt(input, output, "Please enter a port", null); + do{ + clientId = captureMandatoryString(input, output, "Please enter a valid clientId"); + } while(!validClientId()); + } + + public void configureWithHistory() throws IOException { + boolean useConfig = false; + useHistory = true; + if(loadConfig()){ + if(configOk()){ + useConfig = captureMandatoryBoolean(input, output, + String.format("I managed to load port=%s, clientId=%s from history, would you like to use it?", + port, clientId)); + } + } + + if(!useConfig){ + configure(); + } + } + + protected boolean configOk(){ + return hostName != null && port != 0 && clientId != null; + } + + protected void loadConfigHistory(Properties props) throws IOException { + hostName = props.getProperty(HOSTNAME); + port = Integer.valueOf(props.getProperty(PORT)); + clientId = props.getProperty(CLIENTID); + } + + protected void saveConfigHistory(Properties props) { + props.setProperty(HOSTNAME, hostName); + props.setProperty(PORT, String.valueOf(port)); + props.setProperty(CLIENTID, clientId); + } + + protected boolean loadConfig() throws IOException { + Properties properties = new Properties(); + File f = new File("./" + getPropertyFileName()); + if(f.exists()){ + properties.load(new FileInputStream(f)); + loadConfigHistory(properties); + return true; + } + return false; + } + + protected void saveConfig() throws IOException { + Properties properties = new Properties(); + File f = new File("./" + getPropertyFileName()); + if(!f.exists()){ + f.createNewFile(); + } + saveConfigHistory(properties); + properties.store(new FileOutputStream(f), "Generated at " + new Date()); + message("History file written to " + f.getAbsolutePath()); + } + + public void message(String message) throws IOException { + synchronized (output){ + output.println(cli_reset() + "\t>> " + message); + } + } + + public void error(String message, Throwable t) throws IOException { + synchronized (output){ + output.println(cli_red() + "\t>> ERROR " + message); + if(t != null){ + output.println(cli_red() + "\t>> REASON " + t.getMessage()); + t.printStackTrace(); + } + } + } + + protected String cli_reset(){ + return colors ? "\u001B[0m" : ""; + } + + protected String cli_red(){ + return colors ? "\u001B[31m" : ""; + } + + protected String cli_green(){ + return colors ? "\u001B[32m" : ""; + } + + protected String cli_blue(){ + return colors ? "\u001B[34m" : ""; + } + + public void command() throws Exception { + String command = null; + do { + command = captureMandatoryString(input, output, "Please enter a command (or type HELP)"); + try { + if(!processCommand(command)) + closed = true; + } catch(IllegalArgumentException e){ + error("command not found - (type HELP for command list)", null); + } + } while(!closed); + } + + protected boolean validHost(){ + if(hostName == null) return false; + if(hostName.trim().length() == 0) return false; + Pattern p = Pattern.compile("[^a-zA-Z0-9.-]"); + return !p.matcher(hostName).find(); + } + + protected boolean validClientId(){ + if(clientId == null) return false; + if(clientId.trim().length() == 0) return false; + Pattern p = Pattern.compile("[a-zA-Z0-9\\-]{1,1024}"); + return p.matcher(clientId).find(); + } + + protected String getCLIName(){ + return "org.slj interactive command line"; + } + + protected void resetMetrics() throws IOException { + receivedPublishBytesCount.set(0); + sentCount.set(0); + receiveCount.set(0); + sentByteCount.set(0); + receiveByteCount.set(0); + publishedBytesCount.set(0); + message("Metrics & queue reset"); + } + + protected void predefine(String topicName, int alias) + throws IOException { + if(runtime != null && runtimeRegistry != null){ + getOptions().getPredefinedTopics().put(topicName, alias); + message("DONE - predefined topic registered successfully"); + } else { + message("Cannot add a topic to an uninitialised runtime"); + } + } + + protected void stats() throws IOException { + message(String.format("Publish Sent Count: %s messages(s) - (%s bytes) ", sentCount.get(), publishedBytesCount.get())); + message(String.format("Publish Receive Count: %s messages(s) - (%s bytes)", receiveCount.get(), receivedPublishBytesCount.get())); + message(String.format("Network Bytes Sent: %s byte(s)", sentByteCount.get())); + message(String.format("Network Bytes Received: %s byte(s)", receiveByteCount.get())); + } + + protected String captureMandatoryString(Scanner input, PrintStream output, String question){ + String value = null; + while(value == null){ + output.print(cli_reset() + question + " : "); + value = input.nextLine(); + value = value.trim(); + if(value.length() == 0) { + value = null; + } + } + return value; + } + + protected int captureMandatoryInt(Scanner input, PrintStream output, String question, int[] allowedValues){ + String value = null; + while(value == null){ + output.print(cli_reset() + question + " : "); + value = input.nextLine(); + value = value.trim(); + try { + int val = Integer.parseInt(value); + if(allowedValues != null){ + for (int a : allowedValues){ + if(val == a) return val; + } + value = null; + } else { + return val; + } + } catch(NumberFormatException e){ + value = null; + } + } + throw new RuntimeException("cant happen"); + } + + protected boolean captureMandatoryBoolean(Scanner input, PrintStream output, String question){ + String value = null; + while(value == null){ + output.print(cli_reset() + question + " : "); + value = input.nextLine(); + value = value.trim(); + if(value.toLowerCase().equals("y")) return true; + if(value.toLowerCase().equals("n")) return false; + if(value.toLowerCase().equals("yes")) return true; + if(value.toLowerCase().equals("no")) return false; + if(value.toLowerCase().equals("true") || value.toLowerCase().equals("false")){ + return Boolean.parseBoolean(value); + } + value = null; + } + throw new RuntimeException("cant happen"); + } + + public void stop() + throws MqttsnException, IOException { + if(runtime != null){ + runtime.close(); + } + } + + protected void quit() + throws MqttsnException, IOException { + if(runtime != null){ + stop(); + message("stopped - bye :-)"); + } + } + + protected abstract String getPropertyFileName(); + + protected abstract boolean processCommand(String command) throws Exception; + + protected abstract MqttsnOptions createOptions() throws UnknownHostException; + + protected abstract IMqttsnTransport createTransport(); + + protected abstract AbstractMqttsnRuntimeRegistry createRuntimeRegistry(MqttsnOptions options, IMqttsnTransport transport); + + protected abstract AbstractMqttsnRuntime createRuntime(AbstractMqttsnRuntimeRegistry registry, MqttsnOptions options); + + protected AbstractMqttsnRuntimeRegistry getRuntimeRegistry(){ + return runtimeRegistry; + } + + protected AbstractMqttsnRuntime getRuntime(){ + return runtime; + } + + protected MqttsnOptions getOptions(){ + return options; + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnBackoffThreadService.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnBackoffThreadService.java new file mode 100644 index 00000000..ff1c8b53 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnBackoffThreadService.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.impl; + +import org.slj.mqtt.sn.spi.IMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.spi.MqttsnException; +import org.slj.mqtt.sn.spi.MqttsnService; + +import java.util.logging.Level; + +public abstract class AbstractMqttsnBackoffThreadService + extends MqttsnService implements Runnable { + + private Thread t; + private Object monitor = new Object(); + + @Override + public synchronized void start(T runtime) throws MqttsnException { + super.start(runtime); + initThread(); + } + + protected void initThread(){ + if(t == null){ + String name = getDaemonName(); + name = name == null ? getClass().getSimpleName().toLowerCase() : name; + String threadName = String.format("mqtt-sn-deamon-%s", name); + t = new Thread(registry.getRuntime().getThreadGroup(), this, threadName); + t.setPriority(Thread.MIN_PRIORITY); + t.setDaemon(true); + t.start(); + t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + logger.log(Level.SEVERE, "uncaught error on deamon process;", e); + } + }); + } + } + + @Override + public void stop() throws MqttsnException { + super.stop(); + t = null; + } + + @Override + public final void run() { + int count = 1; + try { + registry.getRuntime().joinStartup(); + } catch(Exception e){ + Thread.currentThread().interrupt(); + throw new RuntimeException("error joining startup", e); + } + + logger.log(Level.INFO, String.format("starting thread [%s] processing", Thread.currentThread().getName())); + while(running && + !Thread.currentThread().isInterrupted()){ + long maxBackoff = doWork(); + long waitStart = System.currentTimeMillis(); + synchronized (monitor){ + try { + monitor.wait(Math.max(1, maxBackoff)); + if(logger.isLoggable(Level.FINE)){ + logger.log(Level.FINE, + String.format("worker [%s] waited for [%s] in the end", Thread.currentThread().getName(), System.currentTimeMillis() - waitStart)); + } + } catch(InterruptedException e){ + Thread.currentThread().interrupt(); + } + } + } + logger.log(Level.WARNING, String.format("stopped %s thread", Thread.currentThread().getName())); + } + + /** + * Complete your tasks in this method. + * WARNING, throwing an unchecked exception from this method will cause the service to shutdown + */ + protected abstract long doWork(); + + /** + * The name of the deamon will be used in instrumentation and logging + * @return The name of your deamon process + */ + protected abstract String getDaemonName(); + + protected void expedite(){ + synchronized (monitor){ + monitor.notifyAll(); + } + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnMessageHandler.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnMessageHandler.java new file mode 100644 index 00000000..860e0a19 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnMessageHandler.java @@ -0,0 +1,623 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.impl; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.INetworkContext; +import org.slj.mqtt.sn.model.TopicInfo; +import org.slj.mqtt.sn.spi.*; +import org.slj.mqtt.sn.utils.MqttsnUtils; +import org.slj.mqtt.sn.wire.version1_2.payload.*; + +import java.util.List; +import java.util.logging.Level; + +public abstract class AbstractMqttsnMessageHandler + extends MqttsnService implements IMqttsnMessageHandler { + + public boolean temporaryAuthorizeContext(INetworkContext context) { + try { + IMqttsnContext mqttsnContext = registry.getContextFactory().createTemporaryApplicationContext(context); + if(mqttsnContext != null){ + registry.getNetworkRegistry().bindContexts(context, mqttsnContext); + return true; + } + logger.log(Level.WARNING, String.format("context factory did not provide temporary secured context, refuse auth")); + return false; + } + catch(MqttsnSecurityException e){ + logger.log(Level.WARNING, String.format("security exception detected, refuse auth"), e); + return false; + } + } + + public boolean authorizeContext(INetworkContext context, String clientId) { + try { + registry.getNetworkRegistry().removeExistingClientId(clientId); + IMqttsnContext mqttsnContext = registry.getContextFactory().createInitialApplicationContext(context, clientId); + if(mqttsnContext != null){ + registry.getNetworkRegistry().bindContexts(context, mqttsnContext); + return true; + } + logger.log(Level.WARNING, String.format("context factory did not provide secured context, refuse auth")); + return false; + } + catch(MqttsnSecurityException e){ + logger.log(Level.WARNING, String.format("security exception detected, refuse auth"), e); + return false; + } + } + + @Override + public boolean canHandle(IMqttsnContext context, IMqttsnMessage message){ + return true; + } + + @Override + public boolean validResponse(IMqttsnMessage request, IMqttsnMessage response) { + Class[] clz = getResponseClasses(request); + return MqttsnUtils.contains(clz, response.getClass()); + } + + private Class[] getResponseClasses(IMqttsnMessage message) { + + if(!requiresResponse(message)){ + return new Class[0]; + } + switch(message.getMessageType()){ + case MqttsnConstants.CONNECT: + return new Class[]{ MqttsnConnack.class }; + case MqttsnConstants.PUBLISH: + return new Class[]{ MqttsnPuback.class, MqttsnPubrec.class, MqttsnPubrel.class, MqttsnPubcomp.class }; + case MqttsnConstants.PUBREC: + return new Class[]{ MqttsnPubrel.class }; + case MqttsnConstants.PUBREL: + return new Class[]{ MqttsnPubcomp.class }; + case MqttsnConstants.SUBSCRIBE: + return new Class[]{ MqttsnSuback.class }; + case MqttsnConstants.UNSUBSCRIBE: + return new Class[]{ MqttsnUnsuback.class }; + case MqttsnConstants.REGISTER: + return new Class[]{ MqttsnRegack.class }; + case MqttsnConstants.PINGREQ: + return new Class[]{ MqttsnPingresp.class }; + case MqttsnConstants.DISCONNECT: + return new Class[]{ MqttsnDisconnect.class }; + case MqttsnConstants.SEARCHGW: + return new Class[]{ MqttsnGwInfo.class }; + case MqttsnConstants.WILLMSGREQ: + return new Class[]{ MqttsnWillmsg.class }; + case MqttsnConstants.WILLTOPICREQ: + return new Class[]{ MqttsnWilltopic.class }; + case MqttsnConstants.WILLTOPICUPD: + return new Class[]{ MqttsnWilltopicresp.class }; + case MqttsnConstants.WILLMSGUPD: + return new Class[]{ MqttsnWillmsgresp.class }; + default: + throw new MqttsnRuntimeException( + String.format("invalid message type detected [%s], non terminal and non response!", message.getMessageName())); + } + } + + @Override + public boolean isTerminalMessage(IMqttsnMessage message) { + switch(message.getMessageType()){ + case MqttsnConstants.PUBLISH: + MqttsnPublish publish = (MqttsnPublish) message; + return publish.getQoS() <= 0; + case MqttsnConstants.CONNACK: + case MqttsnConstants.PUBACK: //we delete QoS 1 sent PUBLISH on receipt of PUBACK + case MqttsnConstants.PUBREL: //we delete QoS 2 sent PUBLISH on receipt of PUBREL + case MqttsnConstants.UNSUBACK: + case MqttsnConstants.SUBACK: + case MqttsnConstants.ADVERTISE: + case MqttsnConstants.REGACK: + case MqttsnConstants.PUBCOMP: //we delete QoS 2 received PUBLISH on receipt of PUBCOMP + case MqttsnConstants.PINGRESP: + case MqttsnConstants.DISCONNECT: + case MqttsnConstants.ENCAPSMSG: + case MqttsnConstants.GWINFO: + case MqttsnConstants.WILLMSG: + case MqttsnConstants.WILLMSGRESP: + case MqttsnConstants.WILLTOPIC: + case MqttsnConstants.WILLTOPICRESP: + return true; + default: + return false; + } + } + + @Override + public boolean requiresResponse(IMqttsnMessage message) { + switch(message.getMessageType()){ + case MqttsnConstants.PUBLISH: + MqttsnPublish publish = (MqttsnPublish) message; + return publish.getQoS() > 0; + case MqttsnConstants.CONNECT: + case MqttsnConstants.PUBREC: + case MqttsnConstants.PUBREL: + case MqttsnConstants.SUBSCRIBE: + case MqttsnConstants.UNSUBSCRIBE: + case MqttsnConstants.REGISTER: + case MqttsnConstants.PINGREQ: + case MqttsnConstants.DISCONNECT: + case MqttsnConstants.SEARCHGW: + case MqttsnConstants.WILLMSGREQ: + case MqttsnConstants.WILLMSGUPD: + case MqttsnConstants.WILLTOPICREQ: + case MqttsnConstants.WILLTOPICUPD: + return true; + default: + return false; + } + } + + @Override + public boolean isPartOfOriginatingMessage(IMqttsnMessage message) { + switch(message.getMessageType()){ + case MqttsnConstants.PUBLISH: + case MqttsnConstants.CONNECT: + case MqttsnConstants.SUBSCRIBE: + case MqttsnConstants.UNSUBSCRIBE: + case MqttsnConstants.REGISTER: + case MqttsnConstants.PINGREQ: + case MqttsnConstants.DISCONNECT: + case MqttsnConstants.SEARCHGW: + case MqttsnConstants.WILLMSGREQ: + case MqttsnConstants.WILLMSGUPD: + case MqttsnConstants.WILLTOPICREQ: + case MqttsnConstants.WILLTOPICUPD: + return true; + default: + return false; + } + } + + @Override + public void receiveMessage(IMqttsnContext context, IMqttsnMessage message) + throws MqttsnException { + + try { + + if(!canHandle(context, message)){ + logger.log(Level.WARNING, String.format("mqtt-sn handler [%s] dropping message it could not handle [%s]", + context, message.getMessageName())); + return; + } + + int msgType = message.getMessageType(); + + if(message.isErrorMessage()){ + logger.log(Level.WARNING, String.format("mqtt-sn handler [%s] received error message [%s]", + context, message)); + } + + beforeHandle(context, message); + + if(logger.isLoggable(Level.INFO)){ + logger.log(Level.INFO, String.format("mqtt-sn handler [%s] handling inbound message [%s]", + context, message)); + } + + boolean errord = false; + IMqttsnMessage originatingMessage = null; + + if(registry.getMessageStateService() != null){ + try { + originatingMessage = + registry.getMessageStateService().notifyMessageReceived(context, message); + } catch(MqttsnException e){ + errord = true; + logger.log(Level.WARNING, String.format("mqtt-sn state service errord, allow message lifecycle to handle [%s] -> [%s]", + context, e.getMessage())); + } + } + + IMqttsnMessage response = handle(context, originatingMessage, message, errord); + + //-- if the state service threw a wobbler but for some reason this didnt lead to an error message + //-- we should just disconnect the device + if(errord && !response.isErrorMessage()){ + logger.log(Level.WARNING, String.format("mqtt-sn state service errord, message handler did not produce an error, so overrule and disconnect [%s] -> [%s]", + context, message)); + response = registry.getMessageFactory().createDisconnect(); + } + + //-- this tidies up inflight if there are errors + afterHandle(context, message, response); + + if (response != null) { + if (response.needsMsgId() && response.getMsgId() == 0) { + int msgId = message.getMsgId(); + response.setMsgId(msgId); + } + + handleResponse(context, response); + } + + afterResponse(context, message, response); + + } catch(MqttsnException e){ + logger.log(Level.WARNING,"handled with disconnect error encountered during receive;", e); + handleResponse(context, + registry.getMessageFactory().createDisconnect()); + if(!registry.getRuntime().handleLocalDisconnect(context, e)) { + throw e; + } + } + } + + protected void beforeHandle(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + + } + + protected IMqttsnMessage handle(IMqttsnContext context, IMqttsnMessage originatingMessage, IMqttsnMessage message, boolean errord) + throws MqttsnException { + + IMqttsnMessage response = null; + int msgType = message.getMessageType(); + + switch (msgType) { + case MqttsnConstants.CONNECT: + response = handleConnect(context, message); + if(!errord && !response.isErrorMessage()){ + registry.getRuntime().handleConnected(context); + } + break; + case MqttsnConstants.CONNACK: + if(validateOriginatingMessage(context, originatingMessage, message)){ + handleConnack(context, originatingMessage, message); + if(!errord && !message.isErrorMessage()){ + registry.getRuntime().handleConnected(context); + } + } + break; + case MqttsnConstants.PUBLISH: + if(errord){ + response = getRegistry().getMessageFactory().createPuback(0, + MqttsnConstants.RETURN_CODE_SERVER_UNAVAILABLE); + } else { + response = handlePublish(context, message); + } + break; + case MqttsnConstants.PUBREC: + response = handlePubrec(context, message); + break; + case MqttsnConstants.PUBREL: + response = handlePubrel(context, message); + break; + case MqttsnConstants.PUBACK: + if(validateOriginatingMessage(context, originatingMessage, message)){ + handlePuback(context, originatingMessage, message); + } + break; + case MqttsnConstants.PUBCOMP: + if(validateOriginatingMessage(context, originatingMessage, message)){ + handlePubcomp(context, originatingMessage, message); + } + break; + case MqttsnConstants.SUBSCRIBE: + if(errord){ + response = getRegistry().getMessageFactory().createSuback(0, 0, + MqttsnConstants.RETURN_CODE_SERVER_UNAVAILABLE); + } else { + response = handleSubscribe(context, message); + } + break; + case MqttsnConstants.UNSUBSCRIBE: + response = handleUnsubscribe(context, message); + break; + case MqttsnConstants.UNSUBACK: + if(validateOriginatingMessage(context, originatingMessage, message)){ + if(!errord){ + handleUnsuback(context, originatingMessage, message); + } + } + break; + case MqttsnConstants.SUBACK: + if(validateOriginatingMessage(context, originatingMessage, message)){ + if(!errord){ + handleSuback(context, originatingMessage, message); + } + } + break; + case MqttsnConstants.REGISTER: + if(errord){ + response = getRegistry().getMessageFactory().createRegack(0, + MqttsnConstants.RETURN_CODE_SERVER_UNAVAILABLE); + } else { + response = handleRegister(context, message); + } + break; + case MqttsnConstants.REGACK: + if(validateOriginatingMessage(context, originatingMessage, message)){ + if(!errord){ + handleRegack(context, originatingMessage, message); + } + } + break; + case MqttsnConstants.PINGREQ: + response = handlePingreq(context, message); + break; + case MqttsnConstants.PINGRESP: + if(validateOriginatingMessage(context, originatingMessage, message)){ + handlePingresp(context, originatingMessage, message); + } + break; + case MqttsnConstants.DISCONNECT: + response = handleDisconnect(context, originatingMessage, message); + break; + case MqttsnConstants.ADVERTISE: + handleAdvertise(context, message); + break; + case MqttsnConstants.ENCAPSMSG: + handleEncapsmsg(context, message); + break; + case MqttsnConstants.GWINFO: + handleGwinfo(context, message); + break; + case MqttsnConstants.SEARCHGW: + response = handleSearchGw(context, message); + break; + case MqttsnConstants.WILLMSGREQ: + response = handleWillmsgreq(context, message); + break; + case MqttsnConstants.WILLMSG: + handleWillmsg(context, message); + break; + case MqttsnConstants.WILLMSGUPD: + response = handleWillmsgupd(context, message); + break; + case MqttsnConstants.WILLMSGRESP: + handleWillmsgresp(context, message); + break; + case MqttsnConstants.WILLTOPICREQ: + response = handleWilltopicreq(context, message); + break; + case MqttsnConstants.WILLTOPIC: + handleWilltopic(context, message); + break; + case MqttsnConstants.WILLTOPICUPD: + response = handleWilltopicupd(context, message); + break; + case MqttsnConstants.WILLTOPICRESP: + handleWilltopicresp(context, message); + break; + default: + throw new MqttsnException("unable to handle unknown message type " + msgType); + } + + return response; + } + + + protected void afterHandle(IMqttsnContext context, IMqttsnMessage message, IMqttsnMessage response) throws MqttsnException { + + if(response != null && response.isErrorMessage()){ + //we need to remove any message that was marked inflight + if(message.needsMsgId()){ + if(registry.getMessageStateService().removeInflight(context, message.getMsgId()) != null){ + logger.log(Level.WARNING, "tidied up bad message that was marked inflight and yeilded error response"); + } + } + } + } + + protected void afterResponse(IMqttsnContext context, IMqttsnMessage message, IMqttsnMessage response) throws MqttsnException { + } + + protected boolean validateOriginatingMessage(IMqttsnContext context, IMqttsnMessage originatingMessage, IMqttsnMessage message) { + if(originatingMessage == null){ + logger.log(Level.WARNING, String.format("[%s] no originating message found for acknowledgement [%s]; reaper probably moved this back to queue", context, message)); + return false; + } + return true; + } + + protected void handleResponse(IMqttsnContext context, IMqttsnMessage response) + throws MqttsnException { + + logger.log(Level.INFO, String.format("mqtt-sn handler [%s] sending outbound message [%s]", + context, response)); + registry.getTransport().writeToTransport( + registry.getNetworkRegistry().getContext(context), response); + } + + protected IMqttsnMessage handleConnect(IMqttsnContext context, IMqttsnMessage connect) throws MqttsnException { + + MqttsnConnect connectMessage = (MqttsnConnect) connect ; + if(connectMessage.isWill()){ + return registry.getMessageFactory().createWillTopicReq(); + } else { + return registry.getMessageFactory().createConnack(MqttsnConstants.RETURN_CODE_ACCEPTED); + } + } + + protected void handleConnack(IMqttsnContext context, IMqttsnMessage connect, IMqttsnMessage connack) throws MqttsnException { + } + + protected IMqttsnMessage handleDisconnect(IMqttsnContext context, IMqttsnMessage originatingMessage, IMqttsnMessage message) throws MqttsnException, MqttsnCodecException { + + //-- if the disconnect is received in response to a disconnect we sent, lets not send another! + if(originatingMessage != null){ + logger.log(Level.INFO, "disconnect received in response to my disconnect, dont send another!"); + return null; + } else { + if(registry.getRuntime().handleRemoteDisconnect(context)){ + return registry.getMessageFactory().createDisconnect(); + } + return null; + } + } + + protected IMqttsnMessage handlePingreq(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException, MqttsnCodecException { + return registry.getMessageFactory().createPingresp(); + } + + protected void handlePingresp(IMqttsnContext context, IMqttsnMessage originatingMessage, IMqttsnMessage message) throws MqttsnException { + } + + protected IMqttsnMessage handleSubscribe(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException, MqttsnCodecException { + + MqttsnSubscribe subscribe = (MqttsnSubscribe) message; + return registry.getMessageFactory().createSuback(subscribe.getQoS(), 0x00, MqttsnConstants.RETURN_CODE_ACCEPTED); + } + + protected IMqttsnMessage handleUnsubscribe(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException, MqttsnCodecException { + MqttsnUnsubscribe unsubscribe = (MqttsnUnsubscribe) message; + return registry.getMessageFactory().createUnsuback(); + } + + protected void handleSuback(IMqttsnContext context, IMqttsnMessage initial, IMqttsnMessage message) throws MqttsnException { + MqttsnSuback suback = (MqttsnSuback) message; + if(!suback.isErrorMessage()){ + MqttsnSubscribe subscribe = (MqttsnSubscribe) initial; + String topicPath = null; + if(subscribe.getTopicType() == MqttsnConstants.TOPIC_NORMAL){ + topicPath = subscribe.getTopicName(); + registry.getTopicRegistry().register(context, topicPath, suback.getTopicId()); + } else { + topicPath = registry.getTopicRegistry().topicPath(context, + registry.getTopicRegistry().normalize((byte) subscribe.getTopicType(), subscribe.getTopicData(), false), false); + } + + registry.getSubscriptionRegistry().subscribe(context, topicPath, suback.getQoS()); + } + } + + protected void handleUnsuback(IMqttsnContext context, IMqttsnMessage unsubscribe, IMqttsnMessage unsuback) throws MqttsnException { + String topicPath = ((MqttsnUnsubscribe)unsubscribe).getTopicName(); + registry.getSubscriptionRegistry().unsubscribe(context, topicPath); + } + + protected IMqttsnMessage handleRegister(IMqttsnContext context, IMqttsnMessage message) + throws MqttsnException, MqttsnCodecException { + + MqttsnRegister register = (MqttsnRegister) message; + return registry.getMessageFactory().createRegack(register.getTopicId(), MqttsnConstants.RETURN_CODE_ACCEPTED); + } + + protected void handleRegack(IMqttsnContext context, IMqttsnMessage register, IMqttsnMessage regack) throws MqttsnException { + + String topicPath = ((MqttsnRegister)register).getTopicName(); + registry.getTopicRegistry().register(context, topicPath, ((MqttsnRegack)regack).getTopicId()); + } + + protected IMqttsnMessage handlePublish(IMqttsnContext context, IMqttsnMessage message) + throws MqttsnException, MqttsnCodecException { + + MqttsnPublish publish = (MqttsnPublish) message; + IMqttsnMessage response = null; + + TopicInfo info = registry.getTopicRegistry().normalize((byte) publish.getTopicType(), publish.getTopicData(), false); + String topicPath = registry.getTopicRegistry().topicPath(context, info, true); + if(registry.getPermissionService() != null){ + if(!registry.getPermissionService().allowedToPublish(context, topicPath, publish.getData().length, publish.getQoS())){ + logger.log(Level.WARNING, String.format("permissions service rejected publish from [%s] to [%s]", context, topicPath)); + response = registry.getMessageFactory().createPuback(publish.readTopicDataAsInteger(), + MqttsnConstants.RETURN_CODE_REJECTED_CONGESTION); + } + } + + if(response == null){ + switch (publish.getQoS()) { + case MqttsnConstants.QoS1: + response = registry.getMessageFactory().createPuback(publish.readTopicDataAsInteger(), MqttsnConstants.RETURN_CODE_ACCEPTED); + break; + case MqttsnConstants.QoS2: + response = registry.getMessageFactory().createPubrec(); + break; + + default: + case MqttsnConstants.QoSM1: + case MqttsnConstants.QoS0: + break; + } + } + return response; + } + + protected void handlePuback(IMqttsnContext context, IMqttsnMessage originatingMessage, IMqttsnMessage message) + throws MqttsnException { + } + + protected IMqttsnMessage handlePubrel(IMqttsnContext context, IMqttsnMessage message) + throws MqttsnException, MqttsnCodecException { + return registry.getMessageFactory().createPubcomp(); + } + + protected IMqttsnMessage handlePubrec(IMqttsnContext context, IMqttsnMessage message) + throws MqttsnException, MqttsnCodecException { + return registry.getMessageFactory().createPubrel(); + } + + protected void handlePubcomp(IMqttsnContext context, IMqttsnMessage originatingMessage, IMqttsnMessage message) + throws MqttsnException { + } + + protected void handleAdvertise(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + + } + + protected void handleEncapsmsg(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + } + + protected IMqttsnMessage handleSearchGw(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + return null; + } + + protected void handleGwinfo(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + } + + protected IMqttsnMessage handleWillmsgreq(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + return null; + } + + protected void handleWillmsg(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + } + + protected IMqttsnMessage handleWillmsgupd(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + return null; + } + + protected void handleWillmsgresp(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + } + + protected IMqttsnMessage handleWilltopicreq(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + return null; + } + + protected void handleWilltopic(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + } + + protected IMqttsnMessage handleWilltopicupd(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + return null; + } + + protected void handleWilltopicresp(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnMessageRegistry.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnMessageRegistry.java new file mode 100644 index 00000000..0ec3e523 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnMessageRegistry.java @@ -0,0 +1,117 @@ +package org.slj.mqtt.sn.impl; + +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.spi.*; + +import java.util.Date; +import java.util.UUID; + +public abstract class AbstractMqttsnMessageRegistry + extends MqttsnService implements IMqttsnMessageRegistry { + + @Override + public UUID add(byte[] data, boolean removeAfterRead) throws MqttsnException { + UUID messageId = UUID.randomUUID(); + MessageImpl impl = new MessageImpl(messageId, data, removeAfterRead); + return storeInternal(impl); + } + + @Override + public UUID add(byte[] data, Date expires) throws MqttsnException { + UUID messageId = UUID.randomUUID(); + MessageImpl impl = new MessageImpl(messageId, data, expires); + return storeInternal(impl); + } + + @Override + public byte[] get(UUID messageId) throws MqttsnException { + + MessageImpl impl = readInternal(messageId); + if(impl != null){ + Date expires = impl.getExpires(); + if(expires != null && expires.before(new Date())){ + remove(messageId); + impl = null; + } + } + if(impl == null) throw new MqttsnExpectationFailedException("unable to read message by id, message not found in registry"); + if(impl.isRemoveAfterRead()){ + remove(messageId); + } + return impl.getData(); + } + + + + @Override + public void clear(IMqttsnContext context) throws MqttsnException { + throw new UnsupportedOperationException("message registry is global"); + } + + protected abstract boolean remove(UUID messageId) throws MqttsnException; + + protected abstract UUID storeInternal(MessageImpl message) throws MqttsnException; + + protected abstract MessageImpl readInternal(UUID messageId) throws MqttsnException; + + protected static class MessageImpl { + + Date created; + Date expires; + UUID messageId; + byte[] data; + boolean removeAfterRead = false; + + public MessageImpl(UUID messageId, byte[] data, boolean removeAfterRead) { + this(messageId, data, null); + this.removeAfterRead = removeAfterRead; + } + + public MessageImpl(UUID messageId, byte[] data, Date expires) { + this.created = new Date(); + this.expires = expires; + this.messageId = messageId; + this.data = data; + } + + public boolean isRemoveAfterRead() { + return removeAfterRead; + } + + public void setRemoveAfterRead(boolean removeAfterRead) { + this.removeAfterRead = removeAfterRead; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getExpires() { + return expires; + } + + public void setExpires(Date expires) { + this.expires = expires; + } + + public UUID getMessageId() { + return messageId; + } + + public void setMessageId(UUID messageId) { + this.messageId = messageId; + } + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + } + } +} \ No newline at end of file diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnMessageStateService.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnMessageStateService.java new file mode 100644 index 00000000..6d5d76c9 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnMessageStateService.java @@ -0,0 +1,775 @@ +package org.slj.mqtt.sn.impl; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.PublishData; +import org.slj.mqtt.sn.model.*; +import org.slj.mqtt.sn.spi.*; +import org.slj.mqtt.sn.utils.MqttsnUtils; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; +import org.slj.mqtt.sn.wire.version1_2.payload.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.logging.Level; + +public abstract class AbstractMqttsnMessageStateService + extends AbstractMqttsnBackoffThreadService implements IMqttsnMessageStateService { + + static final int MAX_BACKOFF_INCR = 10; + protected static final Integer WEAK_ATTACH_ID = new Integer(MqttsnConstants.USIGNED_MAX_16 + 1); + protected boolean clientMode; + + protected Map lastActiveMessage; + protected Map lastMessageSent; + protected Map lastMessageReceived; + + protected Map lastUsedMsgIds; + protected Map> flushOperations; + protected ScheduledExecutorService executorService = null; + + public AbstractMqttsnMessageStateService(boolean clientMode) { + this.clientMode = clientMode; + } + + @Override + public synchronized void start(T runtime) throws MqttsnException { + flushOperations = new HashMap(); + executorService = Executors.newScheduledThreadPool( + runtime.getOptions().getStateProcessorThreadCount()); + + lastUsedMsgIds = Collections.synchronizedMap(new HashMap()); + lastMessageReceived = Collections.synchronizedMap(new HashMap()); + lastMessageSent = Collections.synchronizedMap(new HashMap()); + lastActiveMessage = Collections.synchronizedMap(new HashMap()); + super.start(runtime); + } + + @Override + public void stop() throws MqttsnException { + super.stop(); + try { + if(!executorService.isShutdown()){ + executorService.shutdown(); + } + executorService.awaitTermination(10, TimeUnit.SECONDS); + } catch(InterruptedException e){ + Thread.currentThread().interrupt(); + } finally { + if (!executorService.isTerminated()) { + executorService.shutdownNow(); + } + } + } + + protected void scheduleWork(IMqttsnContext context, int time, TimeUnit unit){ + ScheduledFuture future = + executorService.schedule(() -> { + IMqttsnMessageQueueProcessor.RESULT result = + IMqttsnMessageQueueProcessor.RESULT.REMOVE_PROCESS; + boolean process = !flushOperations.containsKey(context) || + !flushOperations.get(context).isDone(); + logger.log(Level.INFO, String.format("processing scheduled work for context [%s] -> [%s]", context, process)); + if(process){ + result = processQueue(context); + switch(result){ + case REMOVE_PROCESS: + synchronized (flushOperations){ + flushOperations.remove(context); + } + logger.log(Level.FINE, String.format("removed context from work list [%s]", context)); + break; + case BACKOFF_PROCESS: + Long lastReceived = lastMessageReceived.get(context); + long delta = lastReceived == null ? 0 : System.currentTimeMillis() - lastReceived; + boolean remove = registry.getOptions().getActiveContextTimeout() < delta; + logger.log(Level.INFO, String.format("backoff requested for [%s], activity delta is [%s], remove work ? [%s]", context, delta, remove)); + if(remove){ + synchronized (flushOperations){ + flushOperations.remove(context); + } + } + else { + scheduleWork(context, Math.max(1000, registry.getOptions().getMinFlushTime()), TimeUnit.MILLISECONDS); + } + break; + case REPROCESS: + scheduleWork(context, registry.getOptions().getMinFlushTime(), TimeUnit.MILLISECONDS); + } + } + return result; + }, time, unit); + synchronized (flushOperations){ + flushOperations.put(context, future); + } + } + + protected IMqttsnMessageQueueProcessor.RESULT processQueue(IMqttsnContext context){ + try { + return registry.getQueueProcessor().process(context); + } catch(Exception e){ + logger.log(Level.SEVERE, + String.format("error encountered processing queue for [%s]", context), e); + return IMqttsnMessageQueueProcessor.RESULT.REMOVE_PROCESS; + } + } + + @Override + public void unscheduleFlush(IMqttsnContext context) { + ScheduledFuture future = null; + if(!flushOperations.containsKey(context)) { + synchronized (flushOperations) { + future = flushOperations.remove(context); + } + } + if(future != null){ + if(!future.isDone()){ + future.cancel(false); + } + } + } + + @Override + public void scheduleFlush(IMqttsnContext context) { + if(!flushOperations.containsKey(context) || + flushOperations.get(context).isDone()){ + logger.log(Level.INFO, String.format("scheduling outbound work for [%s]", context)); + scheduleWork(context, + ThreadLocalRandom.current().nextInt(1, 250), TimeUnit.MILLISECONDS); + } + } + + @Override + protected long doWork() { + + long nextWork = 15000; + + //-- monitor active context timeouts + int activeMessageTimeout = registry.getOptions().getActiveContextTimeout(); + if(activeMessageTimeout > 0){ + synchronized (lastActiveMessage){ + Iterator itr = lastActiveMessage.keySet().iterator(); + while(itr.hasNext()){ + IMqttsnContext context = itr.next(); + Long time = lastActiveMessage.get(context); + if((time + activeMessageTimeout) < System.currentTimeMillis()){ + //-- context is timedout + registry.getRuntime().handleActiveTimeout(context); + itr.remove(); + } + } + } + } + + try { + registry.getMessageRegistry().tidy(); + } catch(Exception e){ + logger.log(Level.SEVERE, "error tidying message registry on state thread;", e); + } + return nextWork; + } + + protected boolean allowedToSend(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + return true; + } + + @Override + public MqttsnWaitToken sendMessage(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + return sendMessageInternal(context, message, null); + } + + @Override + public MqttsnWaitToken sendMessage(IMqttsnContext context, TopicInfo info, QueuedPublishMessage queuedPublishMessage) throws MqttsnException { + + byte[] payload = registry.getMessageRegistry().get(queuedPublishMessage.getMessageId()); + + MqttsnConstants.TOPIC_TYPE type = info.getType(); + int topicId = info.getTopicId(); + if(type == MqttsnConstants.TOPIC_TYPE.SHORT && topicId == 0){ + String topicPath = info.getTopicPath(); + topicId = MqttsnWireUtils.read16bit((byte) topicPath.charAt(0), + (byte) topicPath.charAt(1)); + } + + IMqttsnMessage publish = registry.getMessageFactory().createPublish(queuedPublishMessage.getGrantedQoS(), + queuedPublishMessage.getRetryCount() > 1, false, type, topicId, + payload); + return sendMessageInternal(context, publish, queuedPublishMessage); + } + + protected MqttsnWaitToken sendMessageInternal(IMqttsnContext context, IMqttsnMessage message, QueuedPublishMessage queuedPublishMessage) throws MqttsnException { + + if(!allowedToSend(context, message)){ + logger.log(Level.WARNING, + String.format("allowed to send [%s] check failed [%s]", + message, context)); + throw new MqttsnExpectationFailedException("allowed to send check failed"); + } + + InflightMessage.DIRECTION direction = registry.getMessageHandler().isPartOfOriginatingMessage(message) ? + InflightMessage.DIRECTION.SENDING : InflightMessage.DIRECTION.RECEIVING; + + int count = countInflight(context, direction); + if(count > 0){ + logger.log(Level.WARNING, + String.format("presently unable to send [%s],[%s] to [%s], max inflight reached for direction [%s] [%s] -> [%s]", + message, queuedPublishMessage, context, direction, count, + Objects.toString(getInflightMessages(context)))); + + Optional blockingMessage = + getInflightMessages(context).values().stream().filter(i -> i.getDirection() == direction).findFirst(); + if(blockingMessage.isPresent() && clientMode){ + //-- if we are in client mode, attempt to wait for the ongoing outbound message to complete before we issue next message + MqttsnWaitToken token = blockingMessage.get().getToken(); + if(token != null){ + waitForCompletion(context, token); + if(!token.isError() && token.isComplete()){ + //-- recurse point + return sendMessageInternal(context, message, queuedPublishMessage); + } + else { + logger.log(Level.WARNING, String.format("unable to send, partial send in progress with token [%s]", token)); + throw new MqttsnExpectationFailedException("unable to send message, partial send in progress"); + } + } + } else { + throw new MqttsnExpectationFailedException("max number of inflight messages reached"); + } + } + + try { + + MqttsnWaitToken token = null; + boolean requiresResponse = false; + if((requiresResponse = registry.getMessageHandler().requiresResponse(message))){ + token = markInflight(context, message, queuedPublishMessage); + } + + if(logger.isLoggable(Level.INFO)){ + logger.log(Level.INFO, + String.format("sending message [%s] to [%s] via state service, marking inflight ? [%s]", + message, context, requiresResponse)); + } + + registry.getTransport().writeToTransport(registry.getNetworkRegistry().getContext(context), message); + long time = System.currentTimeMillis(); + if(registry.getCodec().isActiveMessage(message)){ + lastActiveMessage.put(context, time); + } + lastMessageSent.put(context, time); + + //-- the only publish that does not require an ack is QoS so send to app as delivered + if(!requiresResponse && registry.getCodec().isPublish(message)){ + PublishData data = registry.getCodec().getData(message); + CommitOperation op = CommitOperation.outbound(context, queuedPublishMessage.getMessageId(), + queuedPublishMessage.getTopicPath(), queuedPublishMessage.getGrantedQoS(), + data.getData()); + confirmPublish(op); + } + + return token; + + } catch(Exception e){ + throw new MqttsnException("error sending message with confirmations", e); + } + } + + @Override + public Optional waitForCompletion(IMqttsnContext context, final MqttsnWaitToken token) throws MqttsnExpectationFailedException { + return waitForCompletion(context, token, registry.getOptions().getMaxWait()); + } + + @Override + public Optional waitForCompletion(IMqttsnContext context, final MqttsnWaitToken token, int waitTime) throws MqttsnExpectationFailedException { + try { + IMqttsnMessage message = token.getMessage(); + if(token.isComplete()){ + return Optional.ofNullable(message); + } + IMqttsnMessage response = null; + + long start = System.currentTimeMillis(); + long timeToWait = Math.max(waitTime, registry.getOptions().getMaxErrorRetryTime()); + synchronized(token){ + //-- code against spurious wake up + while(!token.isComplete() && + timeToWait > System.currentTimeMillis() - start) { + token.wait(timeToWait); + } + } + + long time = System.currentTimeMillis() - start; + if(token.isComplete()){ + response = token.getResponseMessage(); + if(logger.isLoggable(Level.INFO)){ + logger.log(Level.INFO, String.format("token [%s] in [%s], confirmation of message [%s] -> [%s]", + token.isError() ? "error" : "ok", MqttsnUtils.getDurationString(time), context, response == null ? "" : response)); + } + return Optional.ofNullable(response); + } else { + logger.log(Level.WARNING, String.format("token timed out waiting [%s]ms for response to [%s] in [%s] on thread [%s]", + waitTime, + message, + MqttsnUtils.getDurationString(time), Thread.currentThread().getName())); + token.markError(); + + //a timeout should unblock the sender UNLESS its a PUBLISH in which case this is the jod of the + //reaper (should it be? - surely the sender should monitor..) + try { + clearInflight(context); + } catch(Exception e){ + logger.log(Level.SEVERE, "error cleaning inflight on timeout"); + } + throw new MqttsnExpectationFailedException("unable to obtain response within timeout ("+waitTime+")"); + } + + } catch(InterruptedException e){ + logger.log(Level.WARNING, "a thread waiting for a message being sent was interrupted;", e); + Thread.currentThread().interrupt(); + throw new MqttsnRuntimeException(e); + } + } + + @Override + public IMqttsnMessage notifyMessageReceived(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + + if(registry.getCodec().isActiveMessage(message)){ + lastActiveMessage.put(context, System.currentTimeMillis()); + } + lastMessageReceived.put(context, System.currentTimeMillis()); + + Integer msgId = message.needsMsgId() ? message.getMsgId() : WEAK_ATTACH_ID; + boolean matchedMessage = inflightExists(context, msgId); + boolean terminalMessage = registry.getMessageHandler().isTerminalMessage(message); + if (matchedMessage) { + if (terminalMessage) { + InflightMessage inflight = removeInflight(context, msgId); + if(inflight == null){ + logger.log(Level.WARNING, + String.format("inflight message was cleared during notifyReceive for [%s] -> [%s]", context, msgId)); + return null; + } + else if (!registry.getMessageHandler().validResponse(inflight.getMessage(), message)) { + logger.log(Level.WARNING, + String.format("invalid response message [%s] for [%s] -> [%s]", + message, inflight.getMessage(), context)); + + if(registry.getCodec().isDisconnect(message)){ + + logger.log(Level.WARNING, + String.format("detected distant disconnect, notify application for [%s] -> [%s]", + inflight.getMessage(), context)); + MqttsnWaitToken token = inflight.getToken(); + if (token != null) { + synchronized (token) { + //-- release any waits + token.setResponseMessage(message); + token.markError(); + token.notifyAll(); + } + } + registry.getRuntime().handleRemoteDisconnect(context); + return null; + } else { + throw new MqttsnRuntimeException("invalid response received " + message.getMessageName()); + } + } else { + + IMqttsnMessage confirmedMessage = inflight.getMessage(); + MqttsnWaitToken token = inflight.getToken(); + + if (token != null) { + synchronized (token) { + //-- release any waits + token.setResponseMessage(message); + if (message.isErrorMessage()) token.markError(); + else token.markComplete(); + token.notifyAll(); + } + } + + if (message.isErrorMessage()) { + + logger.log(Level.WARNING, + String.format("error response received [%s] in response to [%s] for [%s]", + message, confirmedMessage, context)); + + //received an error message in response, if its requeuable do so + if (inflight instanceof RequeueableInflightMessage) { + + try { + QueuedPublishMessage m = ((RequeueableInflightMessage) inflight).getQueuedPublishMessage(); + if(m.getRetryCount() >= registry.getOptions().getMaxErrorRetries()){ + logger.log(Level.WARNING, String.format("publish message [%s] exceeded max retries [%s], discard and notify application", registry.getOptions().getMaxErrorRetries(), m)); + PublishData data = registry.getCodec().getData(confirmedMessage); + registry.getRuntime().messageSendFailure(context, m.getMessageId(), m.getTopicPath(), m.getGrantedQoS(), data.getData(), m.getRetryCount()); + } else { + logger.log(Level.INFO, + String.format("message was re-queueable offer to queue [%s]", context)); + registry.getMessageQueue().offer(context, m); + } + } catch(MqttsnQueueAcceptException e){ + throw new MqttsnException(e); + } + } + + } else { + + //inbound qos 2 commit + if (registry.getCodec().isPubRel(message)) { + PublishData data = registry.getCodec().getData(confirmedMessage); + CommitOperation op = CommitOperation.inbound(context, + confirmedMessage instanceof MqttsnPublish ? getTopicPathFromPublish(context, (MqttsnPublish) confirmedMessage) : null, + data.getQos(), + data.getData()); + confirmPublish(op); + } + + //outbound qos 1 + if (registry.getCodec().isPuback(message)) { + RequeueableInflightMessage rim = (RequeueableInflightMessage) inflight; + PublishData data = registry.getCodec().getData(confirmedMessage); + CommitOperation op = CommitOperation.outbound(context, rim.getQueuedPublishMessage().getMessageId(), + rim.getQueuedPublishMessage().getTopicPath(), rim.getQueuedPublishMessage().getGrantedQoS(), + data.getData()); + confirmPublish(op); + } + } + return confirmedMessage; + } + } else { + + InflightMessage inflight = getInflightMessage(context, msgId); + + //none terminal matched message.. this is fine (PUBREC or PUBREL) + //outbound qos 2 commit point + if(inflight != null && registry.getCodec().isPubRec(message)){ + RequeueableInflightMessage rim = (RequeueableInflightMessage) inflight; + PublishData data = registry.getCodec().getData(inflight.getMessage()); + CommitOperation op = CommitOperation.outbound(context, rim.getQueuedPublishMessage().getMessageId(), + rim.getQueuedPublishMessage().getTopicPath(), rim.getQueuedPublishMessage().getGrantedQoS(), + data.getData()); + confirmPublish(op); + } + + return null; + } + + } else { + + //-- received NEW message that was not associated with an inflight message + //-- so we need to pin it into the inflight system (if it needs confirming). + if (registry.getCodec().isPublish(message)) { + PublishData data = registry.getCodec().getData(message); + if (data.getQos() == 2) { +// int count = countInflight(context, InflightMessage.DIRECTION.RECEIVING); +// if(count >= registry.getOptions().getMaxMessagesInflight()){ +// logger.log(Level.WARNING, String.format("have [%s] existing inbound message(s) inflight & new publish QoS2, replacing inflights!", count)); +// throw new MqttsnException("cannot receive more than maxInflight!"); +// } + //-- Qos 2 needs further confirmation before being sent to application + markInflight(context, message, null); + } else { + //-- Qos 0 & 1 are inbound are confirmed on receipt of message + + CommitOperation op = CommitOperation.inbound(context, + message instanceof MqttsnPublish ? getTopicPathFromPublish(context, (MqttsnPublish) message) : null, + data.getQos(), + data.getData()); + confirmPublish(op); + } + } + return null; + } + } + + /** + * Confirmation delivery to the application takes place on the worker thread group + */ + protected void confirmPublish(final CommitOperation operation) { + getRegistry().getRuntime().async(() -> { + IMqttsnContext context = operation.context; + if(operation.inbound){ + registry.getRuntime().messageReceived(context, operation.topicPath, operation.QoS, operation.payload); + } else { + registry.getRuntime().messageSent(context, operation.messageId, operation.topicPath, operation.QoS, operation.payload); + } + }); + } + + protected MqttsnWaitToken markInflight(IMqttsnContext context, IMqttsnMessage message, QueuedPublishMessage queuedPublishMessage) + throws MqttsnException { + + InflightMessage.DIRECTION direction = + message instanceof MqttsnPublish ? + queuedPublishMessage == null ? + InflightMessage.DIRECTION.RECEIVING : InflightMessage.DIRECTION.SENDING : + registry.getMessageHandler().isPartOfOriginatingMessage(message) ? + InflightMessage.DIRECTION.SENDING : InflightMessage.DIRECTION.RECEIVING; + + //may have old inbound messages kicking around depending on reap settings, to just allow these to come in + if(countInflight(context, direction) >= + registry.getOptions().getMaxMessagesInflight()){ + logger.log(Level.WARNING, String.format("[%s] max inflight message number reached, fail-fast for sending, allow for receiving [%s] - [%s]", context, direction, message)); + if(direction == InflightMessage.DIRECTION.SENDING){ + throw new MqttsnExpectationFailedException("max number of inflight messages reached"); + } + } + + InflightMessage inflight = queuedPublishMessage == null ? new InflightMessage(message, direction, MqttsnWaitToken.from(message)) : + new RequeueableInflightMessage(queuedPublishMessage, message); + + LastIdContext idContext = LastIdContext.from(context, direction); + int msgId = WEAK_ATTACH_ID; + if (message.needsMsgId()) { + if (message.getMsgId() > 0) { + msgId = message.getMsgId(); + } else { + msgId = getNextMsgId(idContext); + message.setMsgId(msgId); + } + } + + addInflightMessage(context, msgId, inflight); + + if(logger.isLoggable(Level.INFO)){ + logger.log(Level.INFO, String.format("[%s] marking [%s] message [%s] inflight id context [%s]", context, + direction, message, idContext)); + } + + if(msgId != WEAK_ATTACH_ID) lastUsedMsgIds.put(idContext, msgId); + return inflight.getToken(); + } + + protected Integer getNextMsgId(LastIdContext context) throws MqttsnException { + + Map map = getInflightMessages(context.context); + int startAt = Math.max(lastUsedMsgIds.get(context) == null ? 1 : lastUsedMsgIds.get(context) + 1, + registry.getOptions().getMsgIdStartAt()); + + Set set = map.keySet(); + while(set.contains(new Integer(startAt))){ + startAt = ++startAt % MqttsnConstants.USIGNED_MAX_16; + } + + if(set.contains(new Integer(startAt))) + throw new MqttsnRuntimeException("cannot assign msg id " + startAt); + + if(logger.isLoggable(Level.FINE)){ + logger.log(Level.FINE, String.format("next id available for context [%s] is [%s]", context, startAt)); + } + + return startAt; + } + + public void clearInflight(IMqttsnContext context) throws MqttsnException { + clearInflightInternal(context, 0); + clear(context); + } + + protected void clearInflightInternal(IMqttsnContext context, long evictionTime) throws MqttsnException { + logger.log(Level.FINE, String.format("clearing all inflight messages for context [%s], forced = [%s]", context, evictionTime == 0)); + Map messages = getInflightMessages(context); + if(messages != null && !messages.isEmpty()){ + synchronized (messages){ + Iterator messageItr = messages.keySet().iterator(); + while(messageItr.hasNext()){ + Integer i = messageItr.next(); + InflightMessage f = messages.get(i); + if(f != null){ + if(evictionTime == 0 || + f.getTime() + registry.getOptions().getMaxTimeInflight() < evictionTime){ + if(!registry.getOptions().isReapReceivingMessages() && + f.getDirection() == InflightMessage.DIRECTION.RECEIVING){ + continue; + } + messageItr.remove(); + reapInflight(context, f); + } + } + } + } + } + } + + protected void reapInflight(IMqttsnContext context, InflightMessage inflight) throws MqttsnException { + + IMqttsnMessage message = inflight.getMessage(); + logger.log(Level.WARNING, String.format("clearing message [%s] destined for [%s] aged [%s] from inflight", + message, context, MqttsnUtils.getDurationString(System.currentTimeMillis() - inflight.getTime()))); + + MqttsnWaitToken token = inflight.getToken(); + synchronized (token){ + token.markError(); + token.notifyAll(); + } + + //-- requeue if its a PUBLISH and we have a message queue bound + if(inflight instanceof RequeueableInflightMessage){ + RequeueableInflightMessage requeueableInflightMessage = (RequeueableInflightMessage) inflight; + if(registry.getMessageQueue() != null && + registry.getOptions().isRequeueOnInflightTimeout() && + requeueableInflightMessage.getQueuedPublishMessage() != null) { + logger.log(Level.INFO, String.format("re-queuing publish message [%s] for client [%s]", context, + ((RequeueableInflightMessage) inflight).getQueuedPublishMessage())); + QueuedPublishMessage queuedPublishMessage = requeueableInflightMessage.getQueuedPublishMessage(); + queuedPublishMessage.setToken(null); + try { + boolean maxRetries = queuedPublishMessage.getRetryCount() >= registry.getOptions().getMaxErrorRetries(); + if(maxRetries){ + //-- we're disconnecting the runtime, so reset counter for next active session + queuedPublishMessage.setRetryCount(0); + } + registry.getMessageQueue().offer(context, queuedPublishMessage); + if(maxRetries){ + registry.getRuntime().handleConnectionLost(context, null); + } + } catch(MqttsnQueueAcceptException e){ + throw new MqttsnException(e); + } + } + } + } + + @Override + public int countInflight(IMqttsnContext context, InflightMessage.DIRECTION direction) throws MqttsnException { + Map map = getInflightMessages(context); + Iterator itr = map.keySet().iterator(); + int count = 0; + while(itr.hasNext()){ + Integer i = itr.next(); + InflightMessage msg = map.get(i); + if(msg.getDirection() == direction) count++; + } + return count; + } + + @Override + public boolean canSend(IMqttsnContext context) throws MqttsnException { + return countInflight(context, InflightMessage.DIRECTION.SENDING) == 0; + } + + @Override + public Long getMessageLastSentToContext(IMqttsnContext context) { + return lastMessageSent.get(context); + } + + @Override + public Long getMessageLastReceivedFromContext(IMqttsnContext context) { + return lastMessageReceived.get(context); + } + +// @Override +// public Long getContextLastActive(IMqttsnContext context) throws MqttsnException { +// return lastActiveMessage.get(context); +// } + + protected String getTopicPathFromPublish(IMqttsnContext context, MqttsnPublish publish) throws MqttsnException { + TopicInfo info = registry.getTopicRegistry().normalize((byte) publish.getTopicType(), publish.getTopicData(), false); + String topicPath = registry.getTopicRegistry().topicPath(context, info, true); + return topicPath; + } + + protected abstract void addInflightMessage(IMqttsnContext context, Integer messageId, InflightMessage message) throws MqttsnException ; + + protected abstract InflightMessage getInflightMessage(IMqttsnContext context, Integer messageId) throws MqttsnException ; + + protected abstract Map getInflightMessages(IMqttsnContext context) throws MqttsnException; + + protected abstract boolean inflightExists(IMqttsnContext context, Integer messageId) throws MqttsnException; + + static class CommitOperation { + + protected int QoS; + protected String topicPath; + protected byte[] payload; + protected IMqttsnContext context; + protected long timestamp; + protected UUID messageId; + protected boolean inbound = true; + + public CommitOperation(IMqttsnContext context, long timestamp, String topicPath, int QoS, byte[] payload, boolean inbound) { + this.context = context; + this.timestamp = timestamp; + this.inbound = inbound; + this.topicPath = topicPath; + this.payload = payload; + this.QoS = QoS; + } + + public static CommitOperation inbound(IMqttsnContext context, String topicPath, int QoS, byte[] payload){ + return new CommitOperation(context, System.currentTimeMillis(), topicPath, QoS, payload, true); + } + + public static CommitOperation outbound(IMqttsnContext context, UUID messageId, String topicPath, int QoS, byte[] payload){ + CommitOperation c = new CommitOperation(context, System.currentTimeMillis(), topicPath, QoS, payload, false); + c.messageId = messageId; + return c; + } + } + + static class FlushQueueOperation { + + protected final IMqttsnContext context; + protected long timestamp; + protected int count; + + public FlushQueueOperation(IMqttsnContext context, long timestamp){ + this.context = context; + this.timestamp = timestamp; + } + + public void resetCount(){ + timestamp = 0; + count = 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FlushQueueOperation that = (FlushQueueOperation) o; + return Objects.equals(context, that.context); + } + + @Override + public int hashCode() { + return Objects.hash(context); + } + } + + static class LastIdContext { + + protected final IMqttsnContext context; + protected final InflightMessage.DIRECTION direction; + + public LastIdContext(IMqttsnContext context, InflightMessage.DIRECTION direction) { + this.context = context; + this.direction = direction; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LastIdContext that = (LastIdContext) o; + return context.equals(that.context) && direction == that.direction; + } + + @Override + public int hashCode() { + return Objects.hash(context, direction); + } + + @Override + public String toString() { + return "LastIdContext{" + + "context=" + context + + ", direction=" + direction + + '}'; + } + + public static LastIdContext from(IMqttsnContext context, InflightMessage.DIRECTION direction){ + return new LastIdContext(context, direction); + } + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnRuntime.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnRuntime.java new file mode 100644 index 00000000..505b70d0 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnRuntime.java @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.impl; + +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.spi.*; +import org.slj.mqtt.sn.model.MqttsnOptions; +import org.slj.mqtt.sn.utils.StringTable; +import org.slj.mqtt.sn.utils.StringTableWriters; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.*; +import java.util.concurrent.locks.Condition; +import java.util.logging.Level; +import java.util.logging.Logger; + +public abstract class AbstractMqttsnRuntime { + + protected Logger logger = Logger.getLogger(getClass().getName()); + protected IMqttsnRuntimeRegistry registry; + + protected List receivedListeners + = new ArrayList<>(); + protected List sentListeners + = new ArrayList<>(); + protected List sendFailureListeners + = new ArrayList<>(); + protected List connectionListeners + = new ArrayList<>(); + + protected List activeServices + = Collections.synchronizedList(new ArrayList<>()); + + private ThreadGroup threadGroup = new ThreadGroup("mqtt-sn"); + private Thread instrumentationThread; + protected ExecutorService executorService; + protected CountDownLatch startupLatch; + protected volatile boolean running = false; + protected long startedAt; + private final Object monitor = new Object(); + + public final void start(IMqttsnRuntimeRegistry reg) throws MqttsnException { + start(reg, false); + } + + public final void start(IMqttsnRuntimeRegistry reg, boolean join) throws MqttsnException { + if(!running){ + int threadCount = reg.getOptions().getHandoffThreadCount(); + executorService = + Executors.newFixedThreadPool(threadCount, new ThreadFactory() { + int count = 0; + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(threadGroup, r, "mqtt-sn-worker-thread-" + ++count); + t.setPriority(Thread.MIN_PRIORITY + 1); + t.setDaemon(true); + return t; + } + }); + startedAt = System.currentTimeMillis(); + setupEnvironment(reg.getOptions()); + registry = reg; + startupLatch = new CountDownLatch(1); + running = true; + registry.setRuntime(this); + registry.init(); + bindShutdownHook(); + logger.log(Level.INFO, "starting mqttsn-environment.."); + startupServices(registry); + if(registry.getOptions().isInstrumentationEnabled()){ + initInstrumentation(); + } + startupLatch.countDown(); + logger.log(Level.INFO, String.format("mqttsn-environment started successfully in [%s]", System.currentTimeMillis() - startedAt)); + if(join){ + while(running){ + synchronized (monitor){ + try { + monitor.wait(); + } catch(InterruptedException e){ + Thread.currentThread().interrupt(); + throw new MqttsnException(e); + } + } + } + } + } + } + + public final void stop() throws MqttsnException { + if(running){ + logger.log(Level.INFO, "stopping mqttsn-environment.."); + stopServices(registry); + running = false; + receivedListeners.clear(); + sentListeners.clear(); + sendFailureListeners.clear(); + try { + if(!executorService.isShutdown()){ + executorService.shutdown(); + } + executorService.awaitTermination(30, TimeUnit.SECONDS); + } catch(InterruptedException e){ + Thread.currentThread().interrupt(); + } finally { + if (!executorService.isTerminated()) { + executorService.shutdownNow(); + } + } + synchronized (monitor){ + monitor.notifyAll(); + } + } + } + + protected void initInstrumentation(){ + if(instrumentationThread != null){ + instrumentationThread = new Thread(getThreadGroup(), () -> { + try { + logger.log(Level.INFO, "mqttsn-environment started instrumentation thread"); + while(running){ + synchronized (instrumentationThread){ + instrumentationThread.wait(registry.getOptions().getInstrumentationInterval()); + activeServices.stream(). + filter(s -> s instanceof IMqttsnInstrumentationProvider). + map(s -> (IMqttsnInstrumentationProvider) s).forEach(s -> { + StringTable st = s.provideInstrumentation(); + if(st != null){ + logger.log(Level.INFO, StringTableWriters.writeStringTableAsASCII(st)); + } + }); + } + } + } + catch(InterruptedException e){ + Thread.currentThread().interrupt(); + } + catch(Exception e){ + logger.log(Level.SEVERE, "encountered error tracking instrumentation;", e); + } + }, "mqtt-sn-instrumentation"); + instrumentationThread.setDaemon(true); + instrumentationThread.setPriority(Thread.MIN_PRIORITY); + instrumentationThread.start(); + } + } + + protected void bindShutdownHook(){ + Runtime.getRuntime().addShutdownHook(new Thread(getThreadGroup(), () -> { + try { + AbstractMqttsnRuntime.this.stop(); + } catch(Exception e){ + logger.log(Level.SEVERE, "encountered error executing shutdown hook", e); + } + }, "mqtt-sn-finalizer")); + } + + protected final void callStartup(Object service) throws MqttsnException { + if(service instanceof IMqttsnService){ + IMqttsnService snService = (IMqttsnService) service; + if(!snService.running()){ + logger.log(Level.INFO, String.format("starting [%s]", service.getClass().getName())); + snService.start(registry); + activeServices.add(snService); + } + } + } + + protected final void callShutdown(Object service) throws MqttsnException { + if(service instanceof IMqttsnService){ + IMqttsnService snService = (IMqttsnService) service; + if(snService.running()){ + logger.log(Level.INFO, String.format("stopping [%s]", service.getClass().getName())); + snService.stop(); + activeServices.remove(snService); + } + } + } + + /** + * Allow services to join the startup thread until startup is complete + */ + public final void joinStartup() throws InterruptedException { + startupLatch.await(60, TimeUnit.SECONDS); + } + + public static void setupEnvironment(MqttsnOptions options){ + if(options.getLogPattern() != null){ + System.setProperty("java.util.logging.SimpleFormatter.format", "[%1$tc] %4$s %2$s - %5$s %6$s%n"); + } + } + + protected final void messageReceived(IMqttsnContext context, String topicName, int QoS, byte[] payload){ + if(logger.isLoggable(Level.FINE)){ + logger.log(Level.FINE, String.format("publish received by application [%s], notifying [%s] listeners", topicName, receivedListeners.size())); + } + receivedListeners.stream().forEach(p -> p.receive(context, topicName, QoS, payload)); + } + + protected final void messageSent(IMqttsnContext context, UUID messageId, String topicName, int QoS, byte[] payload){ + if(logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, String.format("sent confirmed by application [%s], notifying [%s] listeners", topicName, sentListeners.size())); + } + sentListeners.stream().forEach(p -> p.sent(context, messageId, topicName, QoS, payload)); + } + + protected final void messageSendFailure(IMqttsnContext context, UUID messageId, String topicName, int QoS, byte[] payload, int retryCount){ + if(logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, String.format("message failed sending [%s], notifying [%s] listeners", topicName, sendFailureListeners.size())); + } + sendFailureListeners.stream().forEach(p -> p.sendFailure(context, messageId, topicName, QoS, payload, retryCount)); + } + + public void registerReceivedListener(IMqttsnPublishReceivedListener listener) { + if(listener == null) throw new IllegalArgumentException("cannot register listener"); + if(!receivedListeners.contains(listener)) + receivedListeners.add(listener); + } + + public void registerSentListener(IMqttsnPublishSentListener listener) { + if(listener == null) throw new IllegalArgumentException("cannot register listener"); + if(!sentListeners.contains(listener)) + sentListeners.add(listener); + } + + public void registerConnectionListener(IMqttsnConnectionStateListener listener) { + if(listener == null) throw new IllegalArgumentException("cannot register listener"); + if(!connectionListeners.contains(listener)) + connectionListeners.add(listener); + } + + public void registerPublishFailedListener(IMqttsnPublishFailureListener listener) { + if(listener == null) throw new IllegalArgumentException("cannot register listener"); + if(!sendFailureListeners.contains(listener)) + sendFailureListeners.add(listener); + } + + + /** + * A Disconnect was received from the remote context + * @param context - The context who sent the DISCONNECT + * @return should the local runtime send a DISCONNECT in reponse + */ + public boolean handleRemoteDisconnect(IMqttsnContext context){ + logger.log(Level.INFO, String.format("notified of remote disconnect [%s]", context)); + connectionListeners.stream().forEach(p -> p.notifyRemoteDisconnect(context)); + return true; + } + + /** + * When the runtime reaches a condition from which it cannot recover for the context, + * it will generate a DISCONNECT to send to the context, the exception and context are then + * passed to this method so the application has visibility of them + * @param context - The context whose state encountered the problem thag caused the DISCONNECT + * @param t - the exception that was encountered + * @return was the exception handled, if so, the trace is not thrown up to the transport layer, + * if not, the exception is reported into the transport layer + */ + public boolean handleLocalDisconnect(IMqttsnContext context, Throwable t){ + logger.log(Level.INFO, String.format("notified of local disconnect [%s]", context, t)); + connectionListeners.stream().forEach(p -> p.notifyLocalDisconnect(context, t)); + return true; + } + + /** + * Reported by the transport layer when its (stateful) connection is lost. Invariably + * this will be Socket connections over TCP IP + * @param context - The context whose state encountered the problem thag caused the DISCONNECT + * @param t - the exception that was encountered + * @return was the exception handled + */ + public void handleConnectionLost(IMqttsnContext context, Throwable t){ + logger.log(Level.INFO, String.format("notified of connection lost [%s]", context, t)); + connectionListeners.stream().forEach(p -> p.notifyConnectionLost(context, t)); + } + + /** + * Reported the when a CONNECTION is successfully established + * @param context + */ + public void handleConnected(IMqttsnContext context){ + logger.log(Level.INFO, String.format("notified of new connection [%s]", context)); + connectionListeners.stream().forEach(p -> p.notifyConnected(context)); + } + + /** + * Reported the when a CONNECTION is successfully established + * @param context + */ + public void handleActiveTimeout(IMqttsnContext context){ + logger.log(Level.INFO, String.format("notified of active timeout [%s]", context)); + connectionListeners.stream().forEach(p -> p.notifyActiveTimeout(context)); + } + + /** + * Submit work for the main worker thread group, this could be + * transport operations or confirmations etc. + */ + public Future async(Runnable r){ + return executorService.submit(r); + } + + /** + * @return - The thread group for this runtime + */ + public ThreadGroup getThreadGroup(){ + return threadGroup; + } + + public abstract void close() throws IOException ; + + protected abstract void startupServices(IMqttsnRuntimeRegistry runtime) throws MqttsnException; + + protected abstract void stopServices(IMqttsnRuntimeRegistry runtime) throws MqttsnException; +} \ No newline at end of file diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnRuntimeRegistry.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnRuntimeRegistry.java new file mode 100644 index 00000000..b6cd90a5 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnRuntimeRegistry.java @@ -0,0 +1,421 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.impl; + +import org.slj.mqtt.sn.model.MqttsnContext; +import org.slj.mqtt.sn.model.MqttsnOptions; +import org.slj.mqtt.sn.net.NetworkContext; +import org.slj.mqtt.sn.net.NetworkAddress; +import org.slj.mqtt.sn.spi.*; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * The base runtime registry provides support for simple fluent construction and encapsulates + * both the controllers and the configuration for a runtime. Each controller has access to the + * registry and uses it to access other parts of the application and config. + * + * During startup, the registry will be validated to ensure all required components are available. + * Extending implementations should provide convenience methods for out-of-box runtimes. + */ +public abstract class AbstractMqttsnRuntimeRegistry implements IMqttsnRuntimeRegistry { + + protected MqttsnOptions options; + + //-- obtained lazily from the codec --// + protected IMqttsnMessageFactory factory; + + protected IMqttsnCodec codec; + protected IMqttsnMessageHandler messageHandler; + protected IMqttsnMessageQueue messageQueue; + protected IMqttsnTransport transport; + protected AbstractMqttsnRuntime runtime; + protected INetworkAddressRegistry networkAddressRegistry; + protected IMqttsnTopicRegistry topicRegistry; + protected IMqttsnSubscriptionRegistry subscriptionRegistry; + protected IMqttsnMessageStateService messageStateService; + protected IMqttsnContextFactory contextFactory; + protected IMqttsnMessageQueueProcessor queueProcessor; + protected IMqttsnQueueProcessorStateService queueProcessorStateCheckService; + protected IMqttsnMessageRegistry messageRegistry; + protected IMqttsnPermissionService permissionService; + protected List trafficListeners; + + public AbstractMqttsnRuntimeRegistry(MqttsnOptions options){ + this.options = options; + } + + @Override + public void init() { + validateOnStartup(); + initNetworkRegistry(); + factory = codec.createMessageFactory(); + } + + protected void initNetworkRegistry(){ + //-- ensure initial definitions exist in the network registry + if(options.getNetworkAddressEntries() != null && !options.getNetworkAddressEntries().isEmpty()){ + Iterator itr = options.getNetworkAddressEntries().keySet().iterator(); + while(itr.hasNext()){ + String key = itr.next(); + NetworkAddress address = options.getNetworkAddressEntries().get(key); + NetworkContext networkContext = new NetworkContext(address); + MqttsnContext sessionContext = new MqttsnContext(key); + networkAddressRegistry.bindContexts(networkContext, sessionContext); + } + } + } + + /** + * Traffic listeners can contributed to the runtime to be notified of any traffic processed by + * the transport layer. Listeners are not able to affect the traffic in transit or the business + * logic executed during the course of the traffic, they are merely observers. + * + * The listeners ~may be notified asynchronously from the application, and thus they cannot be relied + * upon to give an absolute timeline of traffic. + * + * @param trafficListener - The traffic listener instance which will be notified upon traffic being processed. + * @return This runtime registry + */ + public AbstractMqttsnRuntimeRegistry withTrafficListener(IMqttsnTrafficListener trafficListener){ + if(trafficListeners == null){ + synchronized (this){ + if(trafficListeners == null){ + trafficListeners = new ArrayList<>(); + } + } + } + trafficListeners.add(trafficListener); + return this; + } + + /** + * NOTE: this is optional + * When contributed, this controller is used by the queue processor to check if the application is in a fit state to + * offload messages to a remote (gateway or client), and is called back by the queue processor to be notified of + * the queue being empty after having been flushed. + * + * @param queueProcessorStateCheckService - The instance + * @return This runtime registry + */ + public AbstractMqttsnRuntimeRegistry withQueueProcessorStateCheck(IMqttsnQueueProcessorStateService queueProcessorStateCheckService){ + this.queueProcessorStateCheckService = queueProcessorStateCheckService; + return this; + } + + /** + * The job of the queue processor is to (when requested) interact with a remote contexts' queue, processing + * the next message from the HEAD of the queue, handling any topic registration, session state, marking + * messages inflight and finally returning an indicator as to what should happen when the processing of + * the next message is complete. Upon dealing with the next message, whether successful or not, the processor + * needs to return an indiction; + * + * REMOVE_PROCESS - The queue is empty and the context no longer needs further processing + * BACKOFF_PROCESS - The queue is not empty, come back after a backend to try again. Repeating this return type for the same context + * will yield an exponential backoff + * REPROCESS (continue) - The queue is not empty, (where possible) call me back immediately to process again + * + * @param queueProcessor - The instance + * @return This runtime registry + */ + public AbstractMqttsnRuntimeRegistry withQueueProcessor(IMqttsnMessageQueueProcessor queueProcessor){ + this.queueProcessor = queueProcessor; + return this; + } + + /** + * A context factory deals with the initial construction of the context objects which identity + * the remote connection to the application. There are 2 types of context; a {@link NetworkContext} + * and a {@link MqttsnContext}. The network context identifies where (the network location) the identity + * resides and the mqttsn-context identifies who the context is (generally this is the CliendId or GatewayId of + * the connected resource). + * + * A {@link NetworkContext} can exist in isolation without an associated {@link MqttsnContext}, during a CONNECT attempt + * (when the context has yet to be established), or during a failed CONNECTion. An application context cannot exist without + * a network context. + * + * You can provide your own implementation, if you wish to wrap or provide your own extending context implementation + * to wrap custom User objects, for example. + * + * @param contextFactory - The instance + * @return This runtime registry + */ + public AbstractMqttsnRuntimeRegistry withContextFactory(IMqttsnContextFactory contextFactory){ + this.contextFactory = contextFactory; + return this; + } + + /** + * The message registry is a normalised view of transiting messages, it context the raw payload of publish operations + * so light weight references to the payload can exist in multiple storage systems without duplication of data. + * For example, when running in gateway mode, the same message my reside in queues for numerous devices which are + * in different connection states. We should not store payloads N times in this case. + * + * The lookup is a simple UUID -> byte[] relationship. It is up to the registry implementation to decide how to store + * this data. + * + * @param messageRegistry The instance + * @return This runtime registry + */ + public AbstractMqttsnRuntimeRegistry withMessageRegistry(IMqttsnMessageRegistry messageRegistry){ + this.messageRegistry = messageRegistry; + return this; + } + + /** + * The state service is responsible for sending messages and processing received messages. It maintains state + * and tracks messages in and out and their successful acknowledgement (or not). + * + * The message handling layer will call into the state service with messages it has received, and the queue processor + * will use the state service to dispatch new outbound publish messages. + * + * @param messageStateService The instance + * @return This runtime registry + */ + public AbstractMqttsnRuntimeRegistry withMessageStateService(IMqttsnMessageStateService messageStateService){ + this.messageStateService = messageStateService; + return this; + } + + /** + * The subscription registry maintains a list of subscriptions against the remote context. On the gateway this + * is used to determine which clients are subscribed to which topics to enable outbound delivery. In client + * mode it tracks the subscriptions a client presently holds. + * + * @param subscriptionRegistry The instance + * @return This runtime registry + */ + public AbstractMqttsnRuntimeRegistry withSubscriptionRegistry(IMqttsnSubscriptionRegistry subscriptionRegistry){ + this.subscriptionRegistry = subscriptionRegistry; + return this; + } + + /** + * The topic registry is responsible for tracking, storing and determining the correct alias + * to use for a given remote context and topic combination. The topic registry will be cleared + * according to session lifecycle rules. + * + * @param topicRegistry The instance + * @return This runtime registry + */ + public AbstractMqttsnRuntimeRegistry withTopicRegistry(IMqttsnTopicRegistry topicRegistry){ + this.topicRegistry = topicRegistry; + return this; + } + + /** + * Queue implementation to store messages destined to and from gateways and clients. Queues will be flushed acccording + * to the session semantics defined during CONNECT. + * + * Ideally the queue should be implemented to support FIFO where possible. + * + * @param messageQueue The instance + * @return This runtime registry + */ + public AbstractMqttsnRuntimeRegistry withMessageQueue(IMqttsnMessageQueue messageQueue){ + this.messageQueue = messageQueue; + return this; + } + + /** + * A codec contains all the functionality to marshall and unmarshall + * wire traffic in the format specified by the implementation. Further, + * it also provides a message factory instance which allows construction + * of wire messages hiding the underlying transport format. This allows versioned + * protocol support. + * + * @param codec The instance + * @return This runtime registry + */ + public AbstractMqttsnRuntimeRegistry withCodec(IMqttsnCodec codec){ + this.codec = codec; + return this; + } + + /** + * The message handler is delegated to by the transport layer and its job is to process + * inbound messages and marshall into other controllers to manage state lifecycle, authentication, permission + * etc. It is directly responsible for creating response messages and sending them back to the transport layer. + * + * @param handler The instance + * @return This runtime registry + */ + public AbstractMqttsnRuntimeRegistry withMessageHandler(IMqttsnMessageHandler handler){ + this.messageHandler = handler; + return this; + } + + /** + * The transport layer is responsible for managing the receiving and sending of messages over some connection. + * No session is assumed by the application and the connection is considered stateless at this point. + * It is envisaged implementations will include UDP (with and without DTLS), TCP-IP (with and without TLS), + * BLE and ZigBee. + * + * Please refer to {@link org.slj.mqtt.sn.impl.AbstractMqttsnTransport} and sub-class your own implementations + * or choose an existing implementation out of the box. + * + * @see {@link org.slj.mqtt.sn.net.MqttsnUdpTransport} for an example of an out of the box implementation. + * + * @param transport The instance + * @return This runtime registry + */ + public AbstractMqttsnRuntimeRegistry withTransport(IMqttsnTransport transport){ + this.transport = transport; + return this; + } + + /** + * The network registry maintains a list of known network contexts against a remote address ({@link NetworkAddress}). + * It exposes functionality to wait for discovered contexts as well as returning a list of valid broadcast addresses. + * + * @param networkAddressRegistry The instance + * @return This runtime registry + */ + public AbstractMqttsnRuntimeRegistry withNetworkAddressRegistry(INetworkAddressRegistry networkAddressRegistry){ + this.networkAddressRegistry = networkAddressRegistry; + return this; + } + + /** + * Optional - when installed it will be consulted to determine whether a remote context can perform certain + * operations; + * + * CONNECT with the given clientId + * SUBSCRIBE to a given topicPath + * Granted Maximum Subscription Levels + * Eligibility to publish to a given path & size + * + * @param permissionService The instance + * @return This runtime registry + */ + public AbstractMqttsnRuntimeRegistry withPermissionService(IMqttsnPermissionService permissionService){ + this.permissionService = permissionService; + return this; + } + + @Override + public MqttsnOptions getOptions() { + return options; + } + + @Override + public INetworkAddressRegistry getNetworkRegistry() { + return networkAddressRegistry; + } + + @Override + public void setRuntime(AbstractMqttsnRuntime runtime) { + this.runtime = runtime; + } + + @Override + public AbstractMqttsnRuntime getRuntime() { + return runtime; + } + + @Override + public IMqttsnPermissionService getPermissionService() { + return permissionService; + } + + public IMqttsnQueueProcessorStateService getQueueProcessorStateCheckService() { + return queueProcessorStateCheckService; + } + + @Override + public IMqttsnCodec getCodec() { + return codec; + } + + @Override + public IMqttsnMessageHandler getMessageHandler() { + return messageHandler; + } + + @Override + public IMqttsnTransport getTransport() { + return transport; + } + + @Override + public IMqttsnMessageFactory getMessageFactory(){ + return factory; + } + + @Override + public IMqttsnMessageQueue getMessageQueue() { + return messageQueue; + } + + @Override + public IMqttsnTopicRegistry getTopicRegistry() { + return topicRegistry; + } + + @Override + public IMqttsnSubscriptionRegistry getSubscriptionRegistry() { + return subscriptionRegistry; + } + + @Override + public IMqttsnMessageStateService getMessageStateService() { + return messageStateService; + } + + @Override + public IMqttsnMessageRegistry getMessageRegistry(){ + return messageRegistry; + } + + @Override + public List getTrafficListeners() { + return trafficListeners; + } + + @Override + public IMqttsnContextFactory getContextFactory() { + return contextFactory; + } + + @Override + public IMqttsnMessageQueueProcessor getQueueProcessor() { + return queueProcessor; + } + + protected void validateOnStartup() throws MqttsnRuntimeException { + if(networkAddressRegistry == null) throw new MqttsnRuntimeException("network-registry must be bound for valid runtime"); + if(messageStateService == null) throw new MqttsnRuntimeException("message state service must be bound for valid runtime"); + if(transport == null) throw new MqttsnRuntimeException("transport must be bound for valid runtime"); + if(topicRegistry == null) throw new MqttsnRuntimeException("topic registry must be bound for valid runtime"); + if(codec == null) throw new MqttsnRuntimeException("codec must be bound for valid runtime"); + if(messageHandler == null) throw new MqttsnRuntimeException("message handler must be bound for valid runtime"); + if(messageQueue == null) throw new MqttsnRuntimeException("message queue must be bound for valid runtime"); + if(contextFactory == null) throw new MqttsnRuntimeException("context factory must be bound for valid runtime"); + if(queueProcessor == null) throw new MqttsnRuntimeException("queue processor must be bound for valid runtime"); + if(messageRegistry == null) throw new MqttsnRuntimeException("message registry must be bound for valid runtime"); + } +} \ No newline at end of file diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnTransport.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnTransport.java new file mode 100644 index 00000000..2210a40a --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnTransport.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.impl; + +import org.slj.mqtt.sn.model.INetworkContext; +import org.slj.mqtt.sn.spi.*; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +/** + * The abstract transport implementation provides many of the requisite behaviours required of the + * transport layer, including message marshalling, thread handling (handoff), authority checking and traffic listener notification. + * You should sub-class this base as a starting point for you implementations. + */ +public abstract class AbstractMqttsnTransport + extends MqttsnService implements IMqttsnTransport { + + public void connectionLost(INetworkContext context, Throwable t){ + if(registry != null && context != null){ + registry.getRuntime().handleConnectionLost(registry.getNetworkRegistry().getSessionContext(context), t); + } + } + + public boolean restartOnLoss(){ + return true; + } + + @Override + public void receiveFromTransport(INetworkContext context, ByteBuffer buffer) { + if(registry.getOptions().isThreadHandoffFromTransport()){ + getRegistry().getRuntime().async( + () -> receiveFromTransportInternal(context, buffer)); + } else { + receiveFromTransportInternal(context, buffer); + } + } + + protected void receiveFromTransportInternal(INetworkContext networkContext, ByteBuffer buffer) { + try { + if(!registry.getMessageHandler().running()){ + return; + } + byte[] data = drain(buffer); + + if(data.length > registry.getOptions().getMaxProtocolMessageSize()){ + logger.log(Level.SEVERE, String.format("receiving [%s] bytes - max allowed message size [%s] - error", + data.length, registry.getOptions().getMaxProtocolMessageSize())); + throw new MqttsnRuntimeException("received message was larger than allowed max"); + } + + if(registry.getOptions().isWireLoggingEnabled()){ + logger.log(Level.INFO, String.format("receiving [%s] ", + MqttsnWireUtils.toBinary(data))); + } + + IMqttsnMessage message = getRegistry().getCodec().decode(data); + + if(logger.isLoggable(Level.FINE)){ + logger.log(Level.FINE, String.format("receiving [%s] bytes (%s) from [%s] on thread [%s]", + data.length, message.getMessageName(), networkContext, Thread.currentThread().getName())); + } + + boolean authd = true; + //-- if we detect an inbound id packet, we should authorise the context every time (even if the impl just reuses existing auth) + if(message instanceof IMqttsnIdentificationPacket){ + if(!registry.getNetworkRegistry().hasBoundSessionContext(networkContext)){ + authd = registry.getMessageHandler().authorizeContext(networkContext, + ((IMqttsnIdentificationPacket)message).getClientId()); + } + } + else { + //-- sort the case where publish -1 can be recieved without an authd context from the + //-- network address alone + if(!registry.getNetworkRegistry().hasBoundSessionContext(networkContext) && + registry.getCodec().isPublish(message) && + registry.getCodec().getData(message).getQos() == -1){ + logger.log(Level.INFO, String.format("detected non authorised publish -1, apply for temporary auth from network context [%s]", networkContext)); + authd = registry.getMessageHandler().temporaryAuthorizeContext(networkContext); + } + } + + + if(authd && registry.getNetworkRegistry().hasBoundSessionContext(networkContext)){ + notifyTrafficReceived(networkContext, data, message); + registry.getMessageHandler().receiveMessage(registry.getNetworkRegistry().getSessionContext(networkContext), message); + } else { + logger.log(Level.WARNING, "auth could not be established, send disconnect that is not processed by application"); + writeToTransportInternal(networkContext, registry.getMessageFactory().createDisconnect(), false); + } + } catch(Throwable t){ + logger.log(Level.SEVERE, "unhandled error;", t); + } + } + + @Override + public void writeToTransport(INetworkContext context, IMqttsnMessage message) { + if(registry.getOptions().isThreadHandoffFromTransport()){ + getRegistry().getRuntime().async( + () -> writeToTransportInternal(context, message, true)); + } else { + writeToTransportInternal(context, message, true); + } + } + + protected void writeToTransportInternal(INetworkContext context, IMqttsnMessage message, boolean notifyListeners){ + try { + byte[] data = registry.getCodec().encode(message); + + if(data.length > registry.getOptions().getMaxProtocolMessageSize()){ + logger.log(Level.SEVERE, String.format("cannot send [%s] bytes - max allowed message size [%s]", + data.length, registry.getOptions().getMaxProtocolMessageSize())); + throw new MqttsnRuntimeException("cannot send messages larger than allowed max"); + } + + if(logger.isLoggable(Level.FINE)){ + logger.log(Level.FINE, String.format("writing [%s] bytes (%s) to [%s] on thread [%s]", + data.length, message.getMessageName(), context, Thread.currentThread().getName())); + } + + if(registry.getOptions().isWireLoggingEnabled()){ + logger.log(Level.INFO, String.format("writing [%s] ", + MqttsnWireUtils.toBinary(data))); + } + + writeToTransport(context, ByteBuffer.wrap(data, 0 , data.length)); + if(notifyListeners) notifyTrafficSent(context, data, message); + } catch(Throwable e){ + logger.log(Level.SEVERE, String.format("[%s] transport layer errord sending buffer", context), e); + } + } + + private void notifyTrafficReceived(final INetworkContext context, byte[] data, IMqttsnMessage message) { + List list = getRegistry().getTrafficListeners(); + if(list != null && !list.isEmpty()){ + list.stream().forEach(l -> l.trafficReceived(context, data, message)); + } + } + + private void notifyTrafficSent(final INetworkContext context, byte[] data, IMqttsnMessage message) { + List list = getRegistry().getTrafficListeners(); + if(list != null && !list.isEmpty()){ + list.stream().forEach(l -> l.trafficSent(context, data, message)); + } + } + + protected abstract void writeToTransport(INetworkContext context, ByteBuffer data) throws MqttsnException ; + + protected static ByteBuffer wrap(byte[] arr){ + return wrap(arr, arr.length); + } + + protected static ByteBuffer wrap(byte[] arr, int length){ + return ByteBuffer.wrap(arr, 0 , length); + } + + protected static byte[] drain(ByteBuffer buffer){ + byte[] arr = new byte[buffer.remaining()]; + buffer.get(arr, 0, arr.length); + return arr; + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnUdpTransport.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnUdpTransport.java new file mode 100644 index 00000000..66e6070a --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractMqttsnUdpTransport.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.impl; + +import org.slj.mqtt.sn.model.INetworkContext; +import org.slj.mqtt.sn.net.MqttsnUdpOptions; +import org.slj.mqtt.sn.spi.IMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.spi.MqttsnException; + +import java.nio.ByteBuffer; + +public abstract class AbstractMqttsnUdpTransport + extends AbstractMqttsnTransport { + + protected final MqttsnUdpOptions options; + + public AbstractMqttsnUdpTransport(MqttsnUdpOptions options){ + this.options = options; + } + + @Override + public synchronized void start(U runtime) throws MqttsnException { + try { + super.start(runtime); + bind(); + } catch(Exception e){ + throw new MqttsnException(e); + } + } + + public MqttsnUdpOptions getUdpOptions(){ + return options; + } + + public boolean restartOnLoss(){ + return false; + } + + protected abstract void bind() throws Exception; + + public abstract void writeToTransport(INetworkContext context, ByteBuffer buffer) throws MqttsnException ; +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractRationalTopicService.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractRationalTopicService.java new file mode 100644 index 00000000..315139e1 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractRationalTopicService.java @@ -0,0 +1,21 @@ +package org.slj.mqtt.sn.impl; + +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.spi.IMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.spi.MqttsnException; +import org.slj.mqtt.sn.spi.MqttsnService; + +public class AbstractRationalTopicService + extends MqttsnService { + + /** + * Method called before any interactions into or out of the registry to allow for + * hooking to manipulate topic formats + * @param context + * @param topicName + * @return + */ + protected String rationalizeTopic(IMqttsnContext context, String topicName) throws MqttsnException { + return topicName; + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractSubscriptionRegistry.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractSubscriptionRegistry.java new file mode 100644 index 00000000..0424444d --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractSubscriptionRegistry.java @@ -0,0 +1,61 @@ +package org.slj.mqtt.sn.impl; + +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.Subscription; +import org.slj.mqtt.sn.spi.IMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.spi.IMqttsnSubscriptionRegistry; +import org.slj.mqtt.sn.spi.MqttsnException; +import org.slj.mqtt.sn.spi.MqttsnService; +import org.slj.mqtt.sn.utils.TopicPath; + +import java.util.Iterator; +import java.util.Set; + +public abstract class AbstractSubscriptionRegistry + extends AbstractRationalTopicService + implements IMqttsnSubscriptionRegistry { + + @Override + public boolean subscribe(IMqttsnContext context, String topicPath, int QoS) throws MqttsnException { + TopicPath path = new TopicPath(rationalizeTopic(context, topicPath)); + return addSubscription(context, new Subscription(path, QoS)); + } + + @Override + public boolean unsubscribe(IMqttsnContext context, String topicPath) throws MqttsnException { + Set paths = readSubscriptions(context); + TopicPath path = new TopicPath(rationalizeTopic(context, topicPath)); + Subscription sub = new Subscription(path); + if(paths.contains(sub)){ + return removeSubscription(context, sub); + } + return false; + } + + @Override + public int getQos(IMqttsnContext context, String topicPath) throws MqttsnException { + Set paths = readSubscriptions(context); + if(paths != null && !paths.isEmpty()) { + Iterator pathItr = paths.iterator(); + client: + while (pathItr.hasNext()) { + try { + Subscription sub = pathItr.next(); + TopicPath path = sub.getTopicPath(); + if (path.matches(rationalizeTopic(context, topicPath))) { + return sub.getQoS(); + } + } catch (Exception e) { + throw new MqttsnException(e); + } + } + } + throw new MqttsnException("no matching subscription found for client"); + } + + public abstract Set readSubscriptions(IMqttsnContext context) throws MqttsnException ; + + protected abstract boolean addSubscription(IMqttsnContext context, Subscription subscription) throws MqttsnException ; + + protected abstract boolean removeSubscription(IMqttsnContext context, Subscription subscription) throws MqttsnException ; +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractTopicRegistry.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractTopicRegistry.java new file mode 100644 index 00000000..a5c777c5 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/AbstractTopicRegistry.java @@ -0,0 +1,214 @@ +package org.slj.mqtt.sn.impl; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.TopicInfo; +import org.slj.mqtt.sn.spi.*; +import org.slj.mqtt.sn.utils.MqttsnUtils; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; + +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.logging.Level; + +public abstract class AbstractTopicRegistry + extends AbstractRationalTopicService + implements IMqttsnTopicRegistry { + + @Override + public TopicInfo register(IMqttsnContext context, String topicPath) throws MqttsnException { + Map map = getRegistrationsInternal(context, false); + if(map.size() >= registry.getOptions().getMaxTopicsInRegistry()){ + logger.log(Level.WARNING, String.format("max number of registered topics reached for client [%s] >= [%s]", context, map.size())); + throw new MqttsnException("max number of registered topics reached for client"); + } + synchronized (map){ + int alias = 0; + if(map.containsKey(topicPath)){ + alias = map.get(topicPath); + addOrUpdateRegistration(context, rationalizeTopic(context, topicPath), alias); + } else { + alias = MqttsnUtils.getNextLeaseId(map.values(), Math.max(1, registry.getOptions().getAliasStartAt())); + addOrUpdateRegistration(context, rationalizeTopic(context, topicPath), alias); + } + TopicInfo info = new TopicInfo(MqttsnConstants.TOPIC_TYPE.NORMAL, alias); + return info; + } + } + + @Override + public void register(IMqttsnContext context, String topicPath, int topicAlias) throws MqttsnException { + + logger.log(Level.INFO, String.format("registering topic path [%s] -> [%s]", topicPath, topicAlias)); + Map map = getRegistrationsInternal(context, false); + if(map.containsKey(topicPath)){ + //update existing + addOrUpdateRegistration(context, rationalizeTopic(context, topicPath), topicAlias); + } else { + if(map.size() >= registry.getOptions().getMaxTopicsInRegistry()){ + logger.log(Level.WARNING, String.format("max number of registered topics reached for client [%s] >= [%s]", context, map.size())); + throw new MqttsnException("max number of registered topics reached for client"); + } + addOrUpdateRegistration(context, rationalizeTopic(context, topicPath), topicAlias); + } + } + + @Override + public boolean registered(IMqttsnContext context, String topicPath) throws MqttsnException { + Map map = getRegistrationsInternal(context, false); + return map.containsKey(rationalizeTopic(context, topicPath)); + } + + @Override + public String topicPath(IMqttsnContext context, TopicInfo topicInfo, boolean considerContext) throws MqttsnException { + String topicPath = null; + switch (topicInfo.getType()){ + case SHORT: + topicPath = topicInfo.getTopicPath(); + break; + case PREDEFINED: + topicPath = lookupPredefined(context, topicInfo.getTopicId()); + break; + case NORMAL: + if(considerContext){ + if(context == null) throw new MqttsnExpectationFailedException(" context cannot be considered"); + topicPath = lookupRegistered(context, topicInfo.getTopicId()); + } + break; + default: + case RESERVED: + break; + } + + if(topicPath == null) { + logger.log(Level.WARNING, String.format("unable to find matching topicPath in system for [%s] -> [%s], available was [%s]", + topicInfo, context, Objects.toString(getRegistrationsInternal(context, false)))); + throw new MqttsnExpectationFailedException("unable to find matching topicPath in system"); + } + return rationalizeTopic(context, topicPath); + } + + @Override + public String lookupRegistered(IMqttsnContext context, int topicAlias) throws MqttsnException { + Map map = getRegistrationsInternal(context, false); + synchronized (map){ + Iterator itr = map.keySet().iterator(); + while(itr.hasNext()){ + String topicPath = itr.next(); + Integer i = map.get(topicPath); + if(i != null && i.intValue() == topicAlias) + return rationalizeTopic(context, topicPath); + } + } + return null; + } + + @Override + public Integer lookupRegistered(IMqttsnContext context, String topicPath, boolean confirmedOnly) throws MqttsnException { + Map map = getRegistrationsInternal(context, confirmedOnly); + return map.get(rationalizeTopic(context, topicPath)); + } + + @Override + public Integer lookupRegistered(IMqttsnContext context, String topicPath) throws MqttsnException { + Map map = getRegistrationsInternal(context, false); + return map.get(rationalizeTopic(context, topicPath)); + } + + @Override + public Integer lookupPredefined(IMqttsnContext context, String topicPath) throws MqttsnException { + Map predefinedMap = getPredefinedTopicsForString(context); + return predefinedMap.get(rationalizeTopic(context, topicPath)); + } + + @Override + public String lookupPredefined(IMqttsnContext context, int topicAlias) throws MqttsnException { + Map predefinedMap = getPredefinedTopicsForInteger(context); + synchronized (predefinedMap){ + Iterator itr = predefinedMap.keySet().iterator(); + while(itr.hasNext()){ + String topicPath = itr.next(); + Integer i = predefinedMap.get(topicPath); + if(i != null && i.intValue() == topicAlias) + return rationalizeTopic(context, topicPath); + } + } + return null; + } + + @Override + public TopicInfo lookup(IMqttsnContext context, String topicPath, boolean confirmedOnly) throws MqttsnException { + topicPath = rationalizeTopic(context, topicPath); + + //-- check normal first + TopicInfo info = null; + if(registered(context, topicPath)){ + Integer topicAlias = lookupRegistered(context, topicPath, confirmedOnly); + if(topicAlias != null){ + info = new TopicInfo(MqttsnConstants.TOPIC_TYPE.NORMAL, topicAlias); + } + } + + //-- check predefined if nothing in session registry + if(info == null){ + Integer topicAlias = lookupPredefined(context, topicPath); + if(topicAlias != null){ + info = new TopicInfo(MqttsnConstants.TOPIC_TYPE.PREDEFINED, topicAlias); + } + } + + //-- if topicPath < 2 chars + if(info == null){ + if(topicPath.length() <= 2){ + info = new TopicInfo(MqttsnConstants.TOPIC_TYPE.SHORT, topicPath); + } + } + + logger.log(Level.INFO, String.format("topic-registry lookup for [%s] => [%s] found [%s]", context, topicPath, info)); + return info; + } + + @Override + public TopicInfo lookup(IMqttsnContext context, String topicPath) throws MqttsnException { + return lookup(context, topicPath, false); + } + + @Override + public TopicInfo normalize(byte topicIdType, byte[] topicData, boolean normalAsLong) throws MqttsnException { + TopicInfo info = null; + switch (topicIdType){ + case MqttsnConstants.TOPIC_SHORT: + if(topicData.length != 2){ + throw new MqttsnExpectationFailedException("short topics must be exactly 2 bytes"); + } + info = new TopicInfo(MqttsnConstants.TOPIC_TYPE.SHORT, new String(topicData, MqttsnConstants.CHARSET)); + break; + case MqttsnConstants.TOPIC_NORMAL: + if(normalAsLong){ //-- in the case of a subscribe, the normal actually means the full topic name + info = new TopicInfo(MqttsnConstants.TOPIC_TYPE.NORMAL, new String(topicData, MqttsnConstants.CHARSET)); + } else { + if(topicData.length != 2){ + throw new MqttsnExpectationFailedException("normal topic aliases must be exactly 2 bytes"); + } + info = new TopicInfo(MqttsnConstants.TOPIC_TYPE.NORMAL, MqttsnWireUtils.read16bit(topicData[0],topicData[1])); + } + break; + case MqttsnConstants.TOPIC_PREDEFINED: + if(topicData.length != 2){ + throw new MqttsnExpectationFailedException("predefined topic aliases must be exactly 2 bytes"); + } + info = new TopicInfo(MqttsnConstants.TOPIC_TYPE.PREDEFINED, MqttsnWireUtils.read16bit(topicData[0],topicData[1])); + break; + } + return info; + } + + protected abstract boolean addOrUpdateRegistration(IMqttsnContext context, String topicPath, int alias) throws MqttsnException; + + protected abstract Map getRegistrationsInternal(IMqttsnContext context, boolean confirmedOnly) throws MqttsnException; + + protected abstract Map getPredefinedTopicsForInteger(IMqttsnContext context) throws MqttsnException; + + protected abstract Map getPredefinedTopicsForString(IMqttsnContext context) throws MqttsnException; +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/MqttsnContextFactory.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/MqttsnContextFactory.java new file mode 100644 index 00000000..f6254b6a --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/MqttsnContextFactory.java @@ -0,0 +1,43 @@ +package org.slj.mqtt.sn.impl; + +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.INetworkContext; +import org.slj.mqtt.sn.model.MqttsnContext; +import org.slj.mqtt.sn.net.NetworkAddress; +import org.slj.mqtt.sn.net.NetworkContext; +import org.slj.mqtt.sn.spi.*; +import org.slj.mqtt.sn.wire.version1_2.payload.MqttsnConnect; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class MqttsnContextFactory + extends MqttsnService implements IMqttsnContextFactory { + + protected static Logger logger = Logger.getLogger(MqttsnContextFactory.class.getName()); + + @Override + public INetworkContext createInitialNetworkContext(NetworkAddress address) throws MqttsnException { + + logger.log(Level.INFO, + String.format("create new network context for [%s]", address)); + NetworkContext context = new NetworkContext(address); + return context; + } + + @Override + public IMqttsnContext createInitialApplicationContext(INetworkContext networkContext, String clientId) throws MqttsnSecurityException { + + logger.log(Level.INFO, String.format("create new mqtt-sn context for [%s]", clientId)); + MqttsnContext context = new MqttsnContext(clientId); + return context; + } + + @Override + public IMqttsnContext createTemporaryApplicationContext(INetworkContext networkContext) throws MqttsnSecurityException { + + logger.log(Level.INFO, String.format("create temporary mqtt-sn context for [%s]", networkContext)); + MqttsnContext context = new MqttsnContext(null); + return context; + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/MqttsnMessageQueueProcessor.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/MqttsnMessageQueueProcessor.java new file mode 100644 index 00000000..24ce7a34 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/MqttsnMessageQueueProcessor.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.impl; + +import org.slj.mqtt.sn.model.*; +import org.slj.mqtt.sn.spi.*; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class MqttsnMessageQueueProcessor + extends MqttsnService implements IMqttsnMessageQueueProcessor{ + + static Logger logger = Logger.getLogger(MqttsnMessageQueueProcessor.class.getName()); + + protected boolean clientMode; + + public MqttsnMessageQueueProcessor(boolean clientMode) { + this.clientMode = clientMode; + } + + public RESULT process(IMqttsnContext context) throws MqttsnException { + + IMqttsnQueueProcessorStateService stateCheckService = getRegistry().getQueueProcessorStateCheckService(); + + synchronized (context){ + //-- if the queue is empty, then something will happen to retrigger this process, ie. message in or out + //-- so safe to remove + int count = registry.getMessageQueue().size(context); + + if(logger.isLoggable(Level.INFO)){ + logger.log(Level.INFO, + String.format("processing queue size [%s] on thread [%s] in client-mode [%s] for [%s]", count, Thread.currentThread().getName(), clientMode, context)); + } + + if(count == 0){ + if(stateCheckService != null){ + logger.log(Level.INFO, + String.format("notifying state service of queue empty thread [%s] for [%s]", Thread.currentThread().getName(), context)); + //-- this checks on the state of any session and if its AWAKE will lead to a PINGRESP being sent + stateCheckService.queueEmpty(context); + } + return RESULT.REMOVE_PROCESS; + } + + //-- this call checks session state to ensure the client has an active or awake session + if(stateCheckService != null && !stateCheckService.canReceive(context)) { + return RESULT.REMOVE_PROCESS; + } + + //-- this checks the inflight if its > 0 we cannot send + if(!registry.getMessageStateService().canSend(context)) { + logger.log(Level.INFO, String.format("state service determined cant send at the moment [%s]", context)); + return RESULT.BACKOFF_PROCESS; + } + + QueuedPublishMessage queuedMessage = registry.getMessageQueue().peek(context); + if(queuedMessage != null){ + return processNextMessage(context); + } else { + return clientMode ? RESULT.REPROCESS : RESULT.REMOVE_PROCESS; + } + } + } + + /** + * Uses the next message and establshes a register if no support topic alias's exist + */ + protected RESULT processNextMessage(IMqttsnContext context) throws MqttsnException { + + QueuedPublishMessage queuedMessage = registry.getMessageQueue().peek(context); + String topicPath = queuedMessage.getTopicPath(); + TopicInfo info = registry.getTopicRegistry().lookup(context, topicPath, true); + if(info == null){ + logger.log(Level.INFO, String.format("need to register for delivery to [%s] on topic [%s]", context, topicPath)); + if(!clientMode){ + //-- only the server hands out alias's + info = registry.getTopicRegistry().register(context, topicPath); + } + IMqttsnMessage register = registry.getMessageFactory().createRegister(info != null ? info.getTopicId() : 0, topicPath); + try { + MqttsnWaitToken token = registry.getMessageStateService().sendMessage(context, register); + if(clientMode){ + if(token != null){ + registry.getMessageStateService().waitForCompletion(context, token); + } + } + } catch(MqttsnExpectationFailedException e){ + logger.log(Level.WARNING, String.format("unable to send message, try again later"), e); + } + //-- with a register we should come back when the registration is complete and attempt delivery + return RESULT.REPROCESS; + } else { + //-- only deque when we have confirmed we can deliver + return dequeAndPublishNextMessage(context, info); + } + } + + protected RESULT dequeAndPublishNextMessage(IMqttsnContext context, TopicInfo info) throws MqttsnException { + QueuedPublishMessage queuedMessage = registry.getMessageQueue().pop(context); + if (queuedMessage != null) { + queuedMessage.incrementRetry(); + //-- let the reaper check on delivery + try { + MqttsnWaitToken token = registry.getMessageStateService().sendMessage(context, info, queuedMessage); + if (clientMode) { + if(token != null){ + registry.getMessageStateService().waitForCompletion(context, token); + if(token.isError()){ + //error in delivery + throw new MqttsnException("message was not confirmed or confirmed with error"); + } + } + } + + RESULT res = ((registry.getMessageQueue().size(context) > 0) || + queuedMessage.getGrantedQoS() == 0) ? RESULT.REPROCESS : RESULT.REMOVE_PROCESS; + logger.log(Level.INFO, String.format("sending complete returning [%s] for [%s]", res, context)); + return res; + } catch (MqttsnException e) { + + //-- error so back off a little + return RESULT.BACKOFF_PROCESS; + } + } else { + //-- no more messages + return RESULT.REMOVE_PROCESS; + } + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/ram/MqttsnInMemoryMessageQueue.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/ram/MqttsnInMemoryMessageQueue.java new file mode 100644 index 00000000..36fe54fc --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/ram/MqttsnInMemoryMessageQueue.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.impl.ram; + +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.MqttsnQueueAcceptException; +import org.slj.mqtt.sn.model.MqttsnWaitToken; +import org.slj.mqtt.sn.model.QueuedPublishMessage; +import org.slj.mqtt.sn.spi.*; +import org.slj.mqtt.sn.utils.MqttsnUtils; + +import java.util.*; +import java.util.logging.Level; + +public class MqttsnInMemoryMessageQueue + extends MqttsnService implements IMqttsnMessageQueue { + + protected Map> queues; + + @Override + public synchronized void start(T runtime) throws MqttsnException { + queues = Collections.synchronizedMap(new HashMap<>()); + super.start(runtime); + } + + @Override + public int size(IMqttsnContext context) throws MqttsnException { + if (queues.containsKey(context)) { + Queue queue = getQueue(context); + return queue.size(); + } + return 0; + } + + @Override + public MqttsnWaitToken offer(IMqttsnContext context, QueuedPublishMessage message) + throws MqttsnException, MqttsnQueueAcceptException { + Queue queue = getQueue(context); + if(size(context) >= getMaxQueueSize()){ + logger.log(Level.WARNING, String.format("max queue size reached for client [%s] >= [%s]", context, queue.size())); + throw new MqttsnQueueAcceptException("max queue size reached for client"); + } + boolean b; + synchronized (queue){ + b = queue.offer(message); + } + + int size = size(context); + + logger.log(MqttsnUtils.percentOf(size, getMaxQueueSize()) > 80 ? Level.WARNING : Level.FINE, String.format("offered message to queue [%s] for [%s], queue size is [%s]", b, context, queue.size())); + MqttsnWaitToken token = MqttsnWaitToken.from(message); + message.setToken(token); + + if(registry.getMessageStateService() != null) + registry.getMessageStateService().scheduleFlush(context); + + return token; + } + + @Override + public void clear(IMqttsnContext context) throws MqttsnException { + if (queues.containsKey(context)) { + Queue queue = getQueue(context); + synchronized (queue){ + queue.clear(); + } + queues.remove(context); + if(logger.isLoggable(Level.FINE)){ + logger.log(Level.FINE, String.format("clearing queue for [%s]", context)); + } + } + } + + @Override + public void clearAll() throws MqttsnException { + queues.clear(); + } + + @Override + public Iterator listContexts() throws MqttsnException { + synchronized (queues){ + return new ArrayList<>(queues.keySet()).iterator(); + } + } + + @Override + public QueuedPublishMessage pop(IMqttsnContext context) throws MqttsnException { + Queue queue = getQueue(context); + synchronized (queue){ + QueuedPublishMessage p = queue.poll(); + if(logger.isLoggable(Level.FINE)){ + logger.log(Level.FINE, String.format("poll form queue for [%s], queue size is [%s]", context, queue.size())); + } + return p; + } + } + + @Override + public QueuedPublishMessage peek(IMqttsnContext context) throws MqttsnException { + Queue queue = getQueue(context); + synchronized (queue){ + return queue.peek(); + } + } + + protected Queue getQueue(IMqttsnContext context){ + Queue queue = queues.get(context); + if(queue == null){ + synchronized (this){ + if((queue = queues.get(context)) == null){ + //-- queued message uses date for natural sort + queue = new PriorityQueue<>(); + queues.put(context, queue); + } + } + } + return queue; + } + + protected int getMaxQueueSize() { + return registry.getOptions().getMaxMessagesInQueue(); + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/ram/MqttsnInMemoryMessageRegistry.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/ram/MqttsnInMemoryMessageRegistry.java new file mode 100644 index 00000000..0d3635e0 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/ram/MqttsnInMemoryMessageRegistry.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.impl.ram; + +import org.slj.mqtt.sn.impl.AbstractMqttsnMessageRegistry; +import org.slj.mqtt.sn.impl.AbstractTopicRegistry; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.spi.IMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.spi.MqttsnException; + +import java.util.*; +import java.util.logging.Level; + +public class MqttsnInMemoryMessageRegistry + extends AbstractMqttsnMessageRegistry { + + protected Map messageLookup; + + @Override + public synchronized void start(T runtime) throws MqttsnException { + messageLookup = Collections.synchronizedMap(new HashMap<>()); + super.start(runtime); + } + + @Override + protected boolean remove(UUID messageId) throws MqttsnException { + return messageLookup.remove(messageId) != null; + } + + @Override + protected UUID storeInternal(MessageImpl message) throws MqttsnException { + messageLookup.put(message.getMessageId(), message); + return message.getMessageId(); + } + + @Override + protected MessageImpl readInternal(UUID messageId) throws MqttsnException { + return messageLookup.get(messageId); + } + + @Override + public void clearAll() throws MqttsnException { + messageLookup.clear(); + } + + @Override + public void tidy() throws MqttsnException { + Date d = new Date(); + synchronized (messageLookup){ + Iterator itr = messageLookup.keySet().iterator(); + while(itr.hasNext()){ + UUID id = itr.next(); + MessageImpl m = messageLookup.get(id); + if(m.getExpires() != null && m.getExpires().before(d)){ + logger.log(Level.INFO, String.format("expiring message [%s]", id)); + itr.remove(); + } + } + } + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/ram/MqttsnInMemoryMessageStateService.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/ram/MqttsnInMemoryMessageStateService.java new file mode 100644 index 00000000..441d5efe --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/ram/MqttsnInMemoryMessageStateService.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.impl.ram; + +import org.slj.mqtt.sn.impl.AbstractMqttsnMessageStateService; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.InflightMessage; +import org.slj.mqtt.sn.spi.IMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.spi.MqttsnException; + +import java.util.*; +import java.util.logging.Level; + +public class MqttsnInMemoryMessageStateService + extends AbstractMqttsnMessageStateService { + + protected Map> inflightMessages; + + public MqttsnInMemoryMessageStateService(boolean clientMode) { + super(clientMode); + } + + @Override + public synchronized void start(T runtime) throws MqttsnException { + inflightMessages = Collections.synchronizedMap(new HashMap()); + super.start(runtime); + } + + @Override + protected long doWork() { + long nextWork = super.doWork(); + synchronized (inflightMessages) { + Iterator itr = inflightMessages.keySet().iterator(); + while (itr.hasNext()) { + try { + IMqttsnContext context = itr.next(); + clearInflightInternal(context, System.currentTimeMillis()); + } catch(MqttsnException e){ + logger.log(Level.WARNING, "error occurred during inflight eviction run;", e); + } + } + } + return nextWork; + } + + @Override + public void clear(IMqttsnContext context) { + inflightMessages.remove(context); + } + + @Override + public void clearAll() { + inflightMessages.clear(); + } + + @Override + public InflightMessage removeInflight(IMqttsnContext context, int msgId) throws MqttsnException { + Map map = getInflightMessages(context); + return map.remove(msgId); + } + + @Override + protected void addInflightMessage(IMqttsnContext context, Integer messageId, InflightMessage message) throws MqttsnException { + Map map = getInflightMessages(context); + synchronized (map){ + map.put(messageId, message); + } + } + + @Override + protected InflightMessage getInflightMessage(IMqttsnContext context, Integer messageId) throws MqttsnException { + return getInflightMessages(context).get(messageId); + } + + @Override + protected boolean inflightExists(IMqttsnContext context, Integer messageId) throws MqttsnException { + return getInflightMessages(context).containsKey(messageId); + } + + @Override + protected Map getInflightMessages(IMqttsnContext context) { + Map map = inflightMessages.get(context); + if(map == null){ + synchronized (this){ + if((map = inflightMessages.get(context)) == null){ + map = new HashMap<>(); + inflightMessages.put(context, map); + } + } + } + if(logger.isLoggable(Level.FINE)){ + logger.log(Level.FINE, String.format("inflight for [%s] is [%s]", context, Objects.toString(map))); + } + return map; + } + + @Override + protected String getDaemonName() { + return "message-state"; + } +} \ No newline at end of file diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/ram/MqttsnInMemorySubscriptionRegistry.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/ram/MqttsnInMemorySubscriptionRegistry.java new file mode 100644 index 00000000..b2c20070 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/ram/MqttsnInMemorySubscriptionRegistry.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.impl.ram; + +import org.slj.mqtt.sn.impl.AbstractSubscriptionRegistry; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.spi.MqttsnException; +import org.slj.mqtt.sn.model.Subscription; +import org.slj.mqtt.sn.spi.*; +import org.slj.mqtt.sn.utils.TopicPath; + +import java.util.*; + +public class MqttsnInMemorySubscriptionRegistry + extends AbstractSubscriptionRegistry { + + protected Map> subscriptionsLookups; + + @Override + public synchronized void start(T runtime) throws MqttsnException { + subscriptionsLookups = Collections.synchronizedMap(new HashMap()); + super.start(runtime); + } + + @Override + public List matches(String topicPath) throws MqttsnException { + List matchingClients = new ArrayList<>(); + synchronized (subscriptionsLookups){ + Iterator clientItr = subscriptionsLookups.keySet().iterator(); + while(clientItr.hasNext()){ + IMqttsnContext client = clientItr.next(); + Set paths = subscriptionsLookups.get(client); + if(paths != null && !paths.isEmpty()){ + Iterator pathItr = paths.iterator(); + client : while(pathItr.hasNext()) { + try { + Subscription sub = pathItr.next(); + TopicPath path = sub.getTopicPath(); + if(path.matches(topicPath)){ + matchingClients.add(client); + break client; + } + } catch(Exception e){ + throw new MqttsnException(e); + } + } + } + } + } + return matchingClients; + } + + @Override + public Set readSubscriptions(IMqttsnContext context){ + Set set = subscriptionsLookups.get(context); + if(set == null){ + synchronized (this){ + if((set = subscriptionsLookups.get(context)) == null){ + set = new HashSet<>(); + subscriptionsLookups.put(context, set); + } + } + } + return set; + } + + @Override + protected boolean addSubscription(IMqttsnContext context, Subscription subscription) throws MqttsnException { + Set set = readSubscriptions(context); + return set.add(subscription); + } + + @Override + protected boolean removeSubscription(IMqttsnContext context, Subscription subscription) throws MqttsnException { + Set set = readSubscriptions(context); + return set.remove(subscription); + } + + @Override + public void clear(IMqttsnContext context) throws MqttsnException { + subscriptionsLookups.remove(context); + } + + @Override + public void clearAll() throws MqttsnException { + subscriptionsLookups.clear(); + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/ram/MqttsnInMemoryTopicRegistry.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/ram/MqttsnInMemoryTopicRegistry.java new file mode 100644 index 00000000..fa1c4a9e --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/impl/ram/MqttsnInMemoryTopicRegistry.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.impl.ram; + +import org.slj.mqtt.sn.impl.AbstractTopicRegistry; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.MqttsnContext; +import org.slj.mqtt.sn.spi.IMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.spi.MqttsnException; +import org.slj.mqtt.sn.spi.MqttsnExpectationFailedException; + +import java.util.*; +import java.util.logging.Level; + +public class MqttsnInMemoryTopicRegistry + extends AbstractTopicRegistry { + + protected Map> topicLookups; + + @Override + public synchronized void start(T runtime) throws MqttsnException { + topicLookups = Collections.synchronizedMap(new HashMap<>()); + super.start(runtime); + } + + public Set getAll(IMqttsnContext context){ + Set set = topicLookups.get(context); + if(set == null){ + synchronized (this){ + if((set = topicLookups.get(context)) == null){ + set = Collections.synchronizedSet(new HashSet<>()); + topicLookups.put(context, set); + } + } + } + return set; + } + + protected Map getRegistrationsInternal(IMqttsnContext context, boolean confirmedOnly){ + Set set = getAll(context); + Map map = new HashMap<>(); + synchronized (set){ + Iterator itr = set.iterator(); + while(itr.hasNext()){ + ConfirmableTopicRegistration reg = itr.next(); + if(!confirmedOnly || reg.confirmed){ + map.put(reg.topicPath, reg.aliasId); + } + } + } + return map; + } + + @Override + protected boolean addOrUpdateRegistration(IMqttsnContext context, String topicPath, int alias) throws MqttsnException { + + if(topicPath == null || topicPath.trim().length() == 0) + throw new MqttsnExpectationFailedException("null or empty topic path not allowed"); + Set set = getAll(context); + boolean updated = false; + synchronized (set){ + Iterator itr = set.iterator(); + if(itr.hasNext()){ + ConfirmableTopicRegistration reg = itr.next(); + if(reg.topicPath.equals(topicPath)){ + reg.confirmed = true; + reg.setAliasId(alias); + updated = true; + } + } + if(!updated){ + set.add(new ConfirmableTopicRegistration(topicPath, alias, true)); + } + } + + return !updated; + } + + @Override + protected Map getPredefinedTopicsForString(IMqttsnContext context) { + Map m = registry.getOptions().getPredefinedTopics(); + return m == null ? Collections.emptyMap() : m; + } + + @Override + protected Map getPredefinedTopicsForInteger(IMqttsnContext context) { + return getPredefinedTopicsForString(context); + } + + @Override + public void clearAll() throws MqttsnException { + topicLookups.clear(); + } + + @Override + public void clear(IMqttsnContext context, boolean hardClear) throws MqttsnException { + if(hardClear){ + topicLookups.remove(context); + } else{ + Set set = topicLookups.get(context); + if(set != null){ + synchronized (set){ + Iterator itr = set.iterator(); + if(itr.hasNext()){ + ConfirmableTopicRegistration reg = itr.next(); + reg.confirmed = false; + } + } + } + } + } + + @Override + public void clear(IMqttsnContext context) throws MqttsnException { + topicLookups.remove(context); + } + + public static class ConfirmableTopicRegistration { + + boolean confirmed; + String topicPath; + int aliasId; + + public ConfirmableTopicRegistration(String topicPath, int aliasId, boolean confirmed){ + this.topicPath = topicPath; + this.aliasId = aliasId; + this.confirmed = confirmed; + } + + public boolean isConfirmed() { + return confirmed; + } + + public void setConfirmed(boolean confirmed) { + this.confirmed = confirmed; + } + + public String getTopicPath() { + return topicPath; + } + + public void setTopicPath(String topicPath) { + this.topicPath = topicPath; + } + + public int getAliasId() { + return aliasId; + } + + public void setAliasId(int aliasId) { + this.aliasId = aliasId; + } + + @Override + public String toString() { + return "ConfirmableTopicRegistration{" + + "confirmed=" + confirmed + + ", topicPath='" + topicPath + '\'' + + ", aliasId=" + aliasId + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConfirmableTopicRegistration that = (ConfirmableTopicRegistration) o; + return topicPath.equals(that.topicPath); + } + + @Override + public int hashCode() { + int result = topicPath.hashCode(); + return result; + } + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/AbstractContextObject.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/AbstractContextObject.java new file mode 100644 index 00000000..d99287bc --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/AbstractContextObject.java @@ -0,0 +1,52 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.model; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +public abstract class AbstractContextObject implements IContextObject { + + private Map contextObjects = new HashMap<>(); + + public Serializable getContextObject(String contextObjectKey){ + return contextObjects.get(contextObjectKey); + } + + public boolean putContextObject(String contextObjectKey, Serializable contextObject){ + return contextObjects.put(contextObjectKey, contextObject) == null; + } + + @Override + public String toString() { + return "AbstractContextObject{" + + "contextObjects=" + contextObjects + + '}'; + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/IContextObject.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/IContextObject.java new file mode 100644 index 00000000..47fe8d14 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/IContextObject.java @@ -0,0 +1,38 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.model; + +import java.io.Serializable; + +public interface IContextObject extends Serializable { + + Serializable getContextObject(String contextObjectKey); + + boolean putContextObject(String contextObjectKey, Serializable contextObject); + +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/IMqttsnContext.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/IMqttsnContext.java new file mode 100644 index 00000000..e65db120 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/IMqttsnContext.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.model; + +public interface IMqttsnContext extends IContextObject { + + String getId(); +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/IMqttsnSessionState.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/IMqttsnSessionState.java new file mode 100644 index 00000000..f00d52c9 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/IMqttsnSessionState.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.model; + +import java.util.Date; + +public interface IMqttsnSessionState { + + IMqttsnContext getContext(); + + MqttsnClientState getClientState(); + + Date getLastSeen(); + + Date getSessionStarted(); + + int getKeepAlive(); + + void setClientState(MqttsnClientState state); + + void setLastSeen(Date date); + + void setKeepAlive(int keepAlive); +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/INetworkContext.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/INetworkContext.java new file mode 100644 index 00000000..2f7fbc24 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/INetworkContext.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.model; + +import org.slj.mqtt.sn.net.NetworkAddress; + +import java.io.Serializable; + +public interface INetworkContext extends IContextObject { + + int getReceivePort(); + + void setReceivePort(int port); + + NetworkAddress getNetworkAddress(); + + void setNetworkAddress(NetworkAddress address); +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/InflightMessage.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/InflightMessage.java new file mode 100644 index 00000000..5939c4fc --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/InflightMessage.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.model; + +import org.slj.mqtt.sn.spi.IMqttsnMessage; + +import java.io.Serializable; + +public class InflightMessage implements Serializable { + + public static enum DIRECTION {SENDING, RECEIVING} + + transient MqttsnWaitToken token; + IMqttsnMessage message; + long time; + DIRECTION direction; + + public InflightMessage(IMqttsnMessage message, DIRECTION direction, MqttsnWaitToken token) { + this.time = System.currentTimeMillis(); + this.direction = direction; + this.message = message; + this.token = token; + } + + public long getTime(){ + return time; + } + + public IMqttsnMessage getMessage(){ + return message; + } + + public MqttsnWaitToken getToken() { + return token; + } + + public DIRECTION getDirection() { + return direction; + } + + public void setTime(long time) { + this.time = time; + } + + @Override + public String toString() { + return "InflightMessage{" + + "token=" + token + + ", message=" + message + + ", time=" + time + + ", direction=" + direction + + '}'; + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnClientState.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnClientState.java new file mode 100644 index 00000000..b018df14 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnClientState.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.model; + +public enum MqttsnClientState { + PENDING, CONNECTED, DISCONNECTED, AWAKE, ASLEEP, LOST +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnContext.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnContext.java new file mode 100644 index 00000000..16370591 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnContext.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.model; + +import java.util.Objects; + +public class MqttsnContext extends AbstractContextObject implements IMqttsnContext { + + private String id; + + public MqttsnContext(){ + + } + + public MqttsnContext(String id) { + this.id = id; + } + + @Override + public String getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MqttsnContext that = (MqttsnContext) o; + return id.equals(that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return id; + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnOptions.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnOptions.java new file mode 100644 index 00000000..0d08b053 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnOptions.java @@ -0,0 +1,797 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.model; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.net.NetworkAddress; +import org.slj.mqtt.sn.spi.MqttsnRuntimeException; +import org.slj.mqtt.sn.utils.MqttsnUtils; +import org.slj.mqtt.sn.utils.TopicPath; + +import java.util.HashMap; +import java.util.Map; + +/** + * The options class allows you to control aspects of the MQTT-SN engines lifecycle and functionality. The options + * are generally applicable to both client and gateway runtimes, in limited cases, an option may only apply + * to either client OR gateway. + * + * Default values have been specified to be sensible for use in most cases. It is advised that defaults are only + * changed when you have a solid understanding of what you are changing, the imnpacts the changes could have. + */ +public class MqttsnOptions { + + /** + * By default, contexts active message timeout will not be monitored + */ + public static final String DEFAULT_SIMPLE_LOG_PATTERN = "[%1$tc] %4$s %2$s - %5$s %6$s%n"; + + /** + * By default the runtime will NOT remove receiving messages from the state service + */ + public static final boolean DEFAULT_REAP_RECEIVING_MESSAGES = false; + + /** + * The number of retries that shall be attempted before disconnecting the context + */ + public static final int DEFAULT_MAX_ERROR_RETRIES = 3; + + /** + * The interval (in milliseconds) between error retries + */ + public static final int DEFAULT_MAX_ERROR_RETRY_TIME = 10000; + + /** + * By default, contexts active message timeout will not be monitored + */ + public static final int DEFAULT_ACTIVE_CONTEXT_TIMEOUT = 20000; + + /** + * By default, discover is NOT enabled on either the client or the gateway. + */ + public static final boolean DEFAULT_WIRE_LOGGING_ENABLED = false; + + /** + * By default, discover is NOT enabled on either the client or the gateway. + */ + public static final boolean DEFAULT_DISCOVERY_ENABLED = false; + + /** + * By default, thread hand off is enabled on the gateway, and disabled on the client + */ + public static final boolean DEFAULT_THREAD_HANDOFF_ENABLED = true; + + /** + * When thread hand off is enabled, the default number of processing threads is 1 + */ + public static final int DEFAULT_HANDOFF_THREAD_COUNT = 1; + + /** + * Used to handle the outbound queue processing layer, when running as a gateway this should + * scale with the number of expected connected clients + */ + public static final int DEFAULT_STATE_PROCESSOR_THREAD_COUNT = 2; + + /** + * By default, 128 topics can reside in any 1 client registry + */ + public static final int DEFAULT_MAX_TOPICS_IN_REGISTRY = 128; + + /** + * By default, message IDs will start at 1 + */ + public static final int DEFAULT_MSG_ID_STARTS_AT = 1; + + /** + * By default, assigned aliases, handed out by the gateway will start at 1 + */ + public static final int DEFAULT_ALIAS_STARTS_AT = 1; + + /** + * By default, in either direction a client may have 1 message inflight (this is imposed by the specification). + */ + public static final int DEFAULT_MAX_MESSAGES_IN_FLIGHT = 1; + + /** + * By default, 100 publish messages can reside in a client message queue + */ + public static final int DEFAULT_MAX_MESSAGE_IN_QUEUE = 100; + + /** + * By default, timedout messages will be requeued + */ + public static final boolean DEFAULT_REQUEUE_ON_INFLIGHT_TIMEOUT = true; + + /** + * By default, the ASLEEP state will assume topic registrations will need to be reestablished + */ + public static final boolean DEFAULT_SLEEP_CLEARS_REGISTRATIONS = true; + + /** + * The maximum size of a supplied topic is 1024 characters + */ + public static final int DEFAULT_MAX_TOPIC_LENGTH = 1024; + + /** + * The maximum number of entries in a network registry is 1024 + */ + public static final int DEFAULT_MAX_NETWORK_ADDRESS_ENTRIES = 1024; + + /** + * By default, the max wait time for an acknowledgement is 10000 milliseconds + */ + public static final int DEFAULT_MAX_WAIT = 10000; + + /** + * By default, the max time a PUBLISH message will remain in flight is 30000 milliseconds + */ + public static final int DEFAULT_MAX_TIME_INFLIGHT = 30000; + + /** + * By default, the time to wait between activity (receiving and sending) is 1000 milliseconds + */ + public static final int DEFAULT_MIN_FLUSH_TIME = 1000; + + /** + * By default, the discovery search radius is 2 hops + */ + public static final int DEFAULT_SEARCH_GATEWAY_RADIUS = 2; + + /** + * By default, the time in seconds a client waits for a discovered gateway + */ + public static final int DEFAULT_DISCOVERY_TIME_SECONDS = 60 * 60; + + /** + * By default, the divisor is 4 + */ + public static final int DEFAULT_PING_DIVISOR = 4; + + /** + * By default instrumentation is switched off + */ + public static final boolean DEFAULT_INSTRUMENTATION_ENABLED = false; + + /** + * The default instrumentation interval is 60000 (60 seconds) + */ + public static final int DEFAULT_INSTRUMENTATION_INTERVAL = 60000; + + /** + * The default max protocol message size (including header and data) is 1024 bytes + */ + public static final int DEFAULT_MAX_PROTOCOL_SIZE = 1024; + + private String contextId; + private boolean threadHandoffFromTransport = DEFAULT_THREAD_HANDOFF_ENABLED; + private boolean enableDiscovery = DEFAULT_DISCOVERY_ENABLED; + private boolean sleepClearsRegistrations = DEFAULT_SLEEP_CLEARS_REGISTRATIONS; + private int handoffThreadCount = DEFAULT_HANDOFF_THREAD_COUNT; + private int minFlushTime = DEFAULT_MIN_FLUSH_TIME; + private int maxTopicsInRegistry = DEFAULT_MAX_TOPICS_IN_REGISTRY; + private int msgIdStartAt = DEFAULT_MSG_ID_STARTS_AT; + private int aliasStartAt = DEFAULT_ALIAS_STARTS_AT; + private int maxMessagesInflight = DEFAULT_MAX_MESSAGES_IN_FLIGHT; + private int maxMessagesInQueue = DEFAULT_MAX_MESSAGE_IN_QUEUE; + private boolean requeueOnInflightTimeout = DEFAULT_REQUEUE_ON_INFLIGHT_TIMEOUT; + private int maxTopicLength = DEFAULT_MAX_TOPIC_LENGTH; + private int maxNetworkAddressEntries = DEFAULT_MAX_NETWORK_ADDRESS_ENTRIES; + private int maxWait = DEFAULT_MAX_WAIT; + private int maxTimeInflight = DEFAULT_MAX_TIME_INFLIGHT; + private int searchGatewayRadius = DEFAULT_SEARCH_GATEWAY_RADIUS; + private int discoveryTime = DEFAULT_DISCOVERY_TIME_SECONDS; + private int pingDivisor = DEFAULT_PING_DIVISOR; + private boolean instrumentationEnabled = DEFAULT_INSTRUMENTATION_ENABLED; + private int instrumentationInterval = DEFAULT_INSTRUMENTATION_INTERVAL; + private int maxProtocolMessageSize = DEFAULT_MAX_PROTOCOL_SIZE; + private boolean wireLoggingEnabled = DEFAULT_WIRE_LOGGING_ENABLED; + private int activeContextTimeout = DEFAULT_ACTIVE_CONTEXT_TIMEOUT; + private String logPattern = DEFAULT_SIMPLE_LOG_PATTERN; + private int maxErrorRetries = DEFAULT_MAX_ERROR_RETRIES; + private int maxErrorRetryTime = DEFAULT_MAX_ERROR_RETRY_TIME; + private boolean reapReceivingMessages = DEFAULT_REAP_RECEIVING_MESSAGES; + private int stateProcessorThreadCount = DEFAULT_STATE_PROCESSOR_THREAD_COUNT; + + private Map predefinedTopics = new HashMap(); + private Map networkAddressEntries; + + /** + * How many threads should be used to process connected context message queues + * (should scale with the number of expected connected clients and the level + * of concurrency) + * + * @param stateProcessorThreadCount - Number of threads to use to service outbound queue processing + * + * @see {@link MqttsnOptions#DEFAULT_STATE_PROCESSOR_THREAD_COUNT} + * @return this configuration + */ + public MqttsnOptions withStateProcessorThreadCount(int stateProcessorThreadCount){ + this.stateProcessorThreadCount = stateProcessorThreadCount; + return this; + } + + /** + * Should the state service reap messages being recieved? + * + * @param reapReceivingMessages - Reap inbound messages + * + * @see {@link MqttsnOptions#DEFAULT_REAP_RECEIVING_MESSAGES} + * @return this configuration + */ + public MqttsnOptions withReapReceivingMessages(boolean reapReceivingMessages){ + this.reapReceivingMessages = reapReceivingMessages; + return this; + } + + /** + * Configure the log pattern on the environment + * + * @param logPattern - the log pattern applied to the SIMPLE log formatter + * + * @see {@link MqttsnOptions#DEFAULT_SIMPLE_LOG_PATTERN} + * @return this configuration + */ + public MqttsnOptions withLogPattern(String logPattern){ + this.logPattern = logPattern; + return this; + } + + /** + * Configure the behaviour or error retransmissions + * + * @param maxErrorRetries - The max number of retries attempted without a valid response before disconnecting the context + * + * @see {@link MqttsnOptions#DEFAULT_MAX_ERROR_RETRIES} + * @return this configuration + */ + public MqttsnOptions withMaxErrorRetries(int maxErrorRetries){ + this.maxErrorRetries = maxErrorRetries; + return this; + } + + /** + * Configure the behaviour or error retransmissions + * + * @param maxErrorRetryTime - The time between retries when a response is not received + * + * @see {@link MqttsnOptions#DEFAULT_MAX_ERROR_RETRY_TIME} + * @return this configuration + */ + public MqttsnOptions withMaxErrorRetryTime(int maxErrorRetryTime){ + this.maxErrorRetryTime = maxErrorRetryTime; + return this; + } + + /** + * When > 0, active context monitoring will notify the application when the context has not generated + * any active messages (PUBLISH, CONNECT, SUBSCRIBE, UNSUBSCRIBE) + * + * @param activeContextTimeout - the time alllowed between last message SENT or RECEIVED from context before + * notification to the connection listener + * + * @see {@link MqttsnOptions#DEFAULT_ACTIVE_CONTEXT_TIMEOUT} + * @return this configuration + */ + public MqttsnOptions withActiveContextTimeout(int activeContextTimeout){ + this.activeContextTimeout = activeContextTimeout; + return this; + } + + /** + * When enabled, output binary representation of all bytes sent and received + * from transport + * + * @param wireLoggingEnabled - output binary representation of all bytes sent and received from transport + * + * @see {@link MqttsnOptions#DEFAULT_WIRE_LOGGING_ENABLED} + * @return this configuration + */ + public MqttsnOptions withWireLoggingEnabled(boolean wireLoggingEnabled){ + this.wireLoggingEnabled = wireLoggingEnabled; + return this; + } + + /** + * Should the transport layer hand off messages it receives to a processing thread pool so the protocol loop does + * not become blocked by longing running operations. NB: it is advised that the transport loop is kept as quick as + * possible, changing this to false could result is long pauses for concurrent clients. + * + * @see {@link MqttsnOptions#DEFAULT_THREAD_HANDOFF_ENABLED} + * + * @param threadHandoffFromTransport - Should the transport layer hand off messages it receives to a processing thread pool so the protocol loop does + * not become blocked by longing running operations. + * @return this configuration + */ + public MqttsnOptions withThreadHandoffFromTransport(boolean threadHandoffFromTransport){ + this.threadHandoffFromTransport = threadHandoffFromTransport; + return this; + } + + /** + * When threadHandoffFromTransport is set to true, how many threads should be made available in the + * managed pool to handle processing. + * + * @see {@link MqttsnOptions#DEFAULT_HANDOFF_THREAD_COUNT} + * + * @param handoffThreadCount - When threadHandoffFromTransport is set to true, how many threads should be made available in the + * managed pool to handle processing + * @return this configuration + */ + public MqttsnOptions withHandoffThreadCount(int handoffThreadCount){ + this.handoffThreadCount = handoffThreadCount; + return this; + } + + /** + * The idle time between receiving a message and starting a new publish operation (where number of messages on a client queue > 0) + * + * @see {@link MqttsnOptions#DEFAULT_MIN_FLUSH_TIME} + * + * @param minFlushTime - The idle time between receiving a message and starting a new publish operation (where number of messages on a client queue > 0) + * @return this configuration + */ + public MqttsnOptions withMinFlushTime(int minFlushTime){ + this.minFlushTime = minFlushTime; + return this; + } + + /** + * When a client enters the ASLEEP state, should the NORMAL topic registered alias's be cleared down and reestablished during the + * next AWAKE | ACTIVE states. Setting this to false, will mean the gateway will resend REGISTER messages during an AWAKE ping. + * + * @see {@link MqttsnOptions#DEFAULT_SLEEP_CLEARS_REGISTRATIONS} + * + * @param sleepClearsRegistrations - When a client enters the ASLEEP state, should the NORMAL topic registered alias's be cleared down and reestablished during the next AWAKE | ACTIVE states. + * @return this configuration + */ + public MqttsnOptions withSleepClearsRegistrations(boolean sleepClearsRegistrations){ + this.sleepClearsRegistrations = sleepClearsRegistrations; + return this; + } + + /** + * Number of hops to allow broadcast messages + * + * @see {@link MqttsnOptions#DEFAULT_SEARCH_GATEWAY_RADIUS} + * + * @param searchGatewayRadius - Number of hops to allow broadcast messages + * @return this configuration + */ + public MqttsnOptions withSearchGatewayRadius(int searchGatewayRadius){ + this.searchGatewayRadius = searchGatewayRadius; + return this; + } + + /** + * How many messages should be allowed in a client's queue (either to send or buffer from the gateway). + * + * @see {@link MqttsnOptions#DEFAULT_MAX_MESSAGE_IN_QUEUE} + * + * @param maxMessagesInQueue - How many messages should be allowed in a client's queue. + * @return this configuration + */ + public MqttsnOptions withMaxMessagesInQueue(int maxMessagesInQueue){ + this.maxMessagesInQueue = maxMessagesInQueue; + return this; + } + + /** + * When a PUBLISH QoS 1,2 message has been in an unconfirmed state for a period of time, + * should it be requeued for a second DUP sending attempt or discarded. + * + * @see {@link MqttsnOptions#DEFAULT_REQUEUE_ON_INFLIGHT_TIMEOUT} + * + * @param requeueOnInflightTimeout - When a PUBLISH QoS 1,2 message has been in an unconfirmed state for a period of time, + * should it be requeued for a second DUP sending attempt or discarded + * @return this configuration + */ + public MqttsnOptions withRequeueOnInflightTimeout(boolean requeueOnInflightTimeout){ + this.requeueOnInflightTimeout = requeueOnInflightTimeout; + return this; + } + + /** + * Time in millis a PUBLISH message will reside in the INFLIGHT (unconfirmed) state before it is considered DUP (errord). + * + * @see {@link MqttsnOptions#DEFAULT_MAX_TIME_INFLIGHT} + * + * @param maxTimeInflight - Time in millis a PUBLISH message will reside in the INFLIGHT (unconfirmed) state before it is considered DUP (errord). + * @return this configuration + */ + public MqttsnOptions withMaxTimeInflight(int maxTimeInflight){ + this.maxTimeInflight = maxTimeInflight; + return this; + } + + /** + * Maximum number of messages allowed INFLIGHT at any given point in time. NB: the specification allows for a single message in flight in either direction. + * WARNING: changing this default value could lead to unpredictable behaviour depending on the gateway capability. + * + * @see {@link MqttsnOptions#DEFAULT_MAX_MESSAGES_IN_FLIGHT} + * + * @param maxMessagesInflight - Maximum number of messages allowed INFLIGHT at any given point in time. NB: the specification allows for a single message in flight in either direction. + * @return this configuration + */ + public MqttsnOptions withMaxMessagesInflight(int maxMessagesInflight){ + this.maxMessagesInflight = maxMessagesInflight; + return this; + } + + /** + * The maximum time (in millis) that an acknowledged message will wait to be considered successfully confirmed + * by the gateway. + * + * @see {@link MqttsnOptions#DEFAULT_MAX_WAIT} + * + * @param maxWait - Time in millis acknowledged message will wait before an error is thrown + * @return this configuration + */ + public MqttsnOptions withMaxWait(int maxWait){ + this.maxWait = maxWait; + return this; + } + + /** + * Add a predefined topic alias to the registry to be used in all interactions. + * NB: these should be known by both the client and the gateway to enable successful use of + * the PREDEFINED alias types. + * + * @param topicPath - The topic path to register e.g. "foo/bar" + * @param alias - The alias of the topic path to match + * @return this configuration + */ + public MqttsnOptions withPredefinedTopic(String topicPath, int alias){ + if(!TopicPath.isValidTopic(topicPath, Math.max(maxTopicLength, MqttsnConstants.USIGNED_MAX_16))){ + throw new MqttsnRuntimeException("invalid topic path " + topicPath); + } + + if(!MqttsnUtils.validUInt16(alias)){ + throw new MqttsnRuntimeException("invalid topic alias " + alias); + } + +// if(predefinedTopics == null){ +// synchronized (this) { +// if (predefinedTopics == null) { +// predefinedTopics = new HashMap(); +// } +// } +// } + predefinedTopics.put(topicPath, alias); + return this; + } + + /** + * The maximum length of a topic allowed, including all wildcard or separator characters. + * + * @see {@link MqttsnOptions#DEFAULT_MAX_TOPIC_LENGTH} + * + * @param maxTopicLength - The maximum length of a topic allowed, including all wildcard or separator characters. + * @return this configuration + */ + public MqttsnOptions withMaxTopicLength(int maxTopicLength){ + this.maxTopicLength = maxTopicLength; + return this; + } + + /** + * The number at which messageIds start, typically this should be 1. + * + * @see {@link MqttsnOptions#DEFAULT_MSG_ID_STARTS_AT} + * + * @param msgIdStartAt - The number at which messageIds start, typically this should be 1. + * @return this configuration + */ + public MqttsnOptions withMsgIdsStartAt(int msgIdStartAt){ + this.msgIdStartAt = msgIdStartAt; + return this; + } + + /** + * The maximum number of NORMAL topics allowed in the topic registry. + * NB: Realistically an application should not need many hundreds of topics in their hierarchy + * + * @see {@link MqttsnOptions#DEFAULT_MAX_TOPICS_IN_REGISTRY} + * + * @param maxTopicsInRegistry - The maximum number of NORMAL topics allowed in the topic registry. + * @return this configuration + */ + public MqttsnOptions withMaxTopicsInRegistry(int maxTopicsInRegistry){ + this.maxTopicsInRegistry = maxTopicsInRegistry; + return this; + } + + /** + * Should discovery be enabled. When enabled the transport layer will run its broadcast threads and + * allow dynamic gateway / client binding. + * + * @see {@link MqttsnOptions#DEFAULT_DISCOVERY_ENABLED} + * + * @param enableDiscovery - Should discovery be enabled. + * @return this configuration + */ + public MqttsnOptions withDiscoveryEnabled(boolean enableDiscovery){ + this.enableDiscovery = enableDiscovery; + return this; + } + + /** + * The maximum number of addresses allowed in the network registry. An address is a network location mapped + * to a clientId + * + * @see {@link MqttsnOptions#DEFAULT_MAX_NETWORK_ADDRESS_ENTRIES} + * + * @param maxNetworkAddressEntries - The maximum number of addresses allowed in the network registry + * @return this configuration + */ + public MqttsnOptions withMaxNetworkAddressEntries(int maxNetworkAddressEntries){ + this.maxNetworkAddressEntries = maxNetworkAddressEntries; + return this; + } + + /** + * The starting value of assigned NORMAL topic aliases that the gateway hands out. + * + * @see {@link MqttsnOptions#DEFAULT_ALIAS_STARTS_AT} + * + * @param aliasStartAt - The starting value of assigned NORMAL topic aliases that the gateway hands out. + * @return this configuration + */ + public MqttsnOptions withAliasStartAt(int aliasStartAt){ + this.aliasStartAt = aliasStartAt; + return this; + } + + /** + * The contextId is a general term for EITHER the clientId when running as a client or the gatewayId + * when running as a gateway. It should conform to the specification. It is advised that it contains between + * 1-23 alpha numeric characters from the ASCII character set. + * + * NB: When running in gateway mode, this is a mandatory item that should be set by the application. + * + * @param contextId - The contextId is a general term for EITHER the clientId when running as a client or the gatewayId + * when running as a gateway. + * @return this configuration + */ + public MqttsnOptions withContextId(String contextId){ + this.contextId = contextId; + return this; + } + + /** + * The time (in seconds) a client will wait for a broadcast during CONNECT before giving up + * + * NB: only applicable to client + * + * @see {@link MqttsnOptions#DEFAULT_DISCOVERY_TIME_SECONDS} + * + * @param discoveryTime - Time (in seconds) a client will wait for a broadcast during CONNECT before giving up + * when running as a client. + * @return this configuration + */ + public MqttsnOptions withDiscoveryTime(int discoveryTime){ + this.discoveryTime = discoveryTime; + return this; + } + + /** + * The divisor to use for the ping window, the dividend being the CONNECT keepAlive resulting + * in the quotient which is the time (since last sent message) each ping will be issued + * + * For example a 60 seconds session with a divisor of 4 will yield 15 second pings between + * activity + * + * NB: only applicable to client + * + * @see {@link MqttsnOptions#DEFAULT_PING_DIVISOR} + * + * @param pingDivisor - The divisor to use for the ping window + * @return this configuration + */ + public MqttsnOptions withPingDivisor(int pingDivisor){ + this.pingDivisor = pingDivisor; + return this; + } + + /** + * Should instrumentation be enabled. When enabled the runtime will call registered instrumentation + * providers on the {@link MqttsnOptions#getInstrumentationInterval} period and output the data + * to the standard logging. + * + * @param instrumentationEnabled - Should instrumentation be enabled + * @return this configuration + */ + public MqttsnOptions withInstrumentationEnabled(boolean instrumentationEnabled){ + this.instrumentationEnabled = instrumentationEnabled; + return this; + } + + /** + * The interval between instrumentation sampling when it is enabled. + * + * @param instrumentationInterval The interval between instrumentation sampling when it is enabled. + * @return this configuration + */ + public MqttsnOptions withInstrumentationInterval(int instrumentationInterval){ + this.instrumentationInterval = instrumentationInterval; + return this; + } + + /** + * The max allowable size of protocol messages that will be sent or received by the system. + * NOTE: this differs from transport level max sizes which will be deterimed and constrained by the + * MTU of the transport + * + * @param maxProtocolMessageSize - The max allowable size of protocol messages. + * @return this configuration + */ + public MqttsnOptions withMaxProtocolMessageSize(int maxProtocolMessageSize){ + this.maxProtocolMessageSize = maxProtocolMessageSize; + return this; + } + + /** + * Sets the locations of known clients or gateways on the network. When running as a client and discovery is not enabled, + * it is mandatory that at least 1 gateway entry be supplied, which will be the gateway the client talks to. In gateway + * mode, the registry is populated dynamically. + * @param contextId - the contextId of the known remote location + * @param address - the network address of the known remote location + * @return this config + */ + public MqttsnOptions withNetworkAddressEntry(String contextId, NetworkAddress address){ + if(networkAddressEntries == null){ + synchronized (this) { + if (networkAddressEntries == null) { + networkAddressEntries = new HashMap(); + } + } + } + networkAddressEntries.put(contextId, address); + return this; + } + + public Map getNetworkAddressEntries() { + return networkAddressEntries; + } + + public int getAliasStartAt() { + return aliasStartAt; + } + + public int getMsgIdStartAt() { + return msgIdStartAt; + } + + public int getMaxTopicsInRegistry() { + return maxTopicsInRegistry; + } + + public boolean isEnableDiscovery() { + return enableDiscovery; + } + + public int getMaxTopicLength() { + return maxTopicLength; + } + + public String getContextId() { + return contextId; + } + + public int getMaxTimeInflight() { + return maxTimeInflight; + } + + public int getMaxNetworkAddressEntries() { + return maxNetworkAddressEntries; + } + + public Map getPredefinedTopics() { + return predefinedTopics; + } + + public int getMaxMessagesInflight() { + return maxMessagesInflight; + } + + public int getMaxMessagesInQueue() { + return maxMessagesInQueue; + } + + public int getMaxWait() { + return maxWait; + } + + public int getHandoffThreadCount() { + return handoffThreadCount; + } + + public int getSearchGatewayRadius() { + return searchGatewayRadius; + } + + public int getMinFlushTime() { + return minFlushTime; + } + + public boolean isSleepClearsRegistrations() { + return sleepClearsRegistrations; + } + + public int getDiscoveryTime() { + return discoveryTime; + } + + public int getPingDivisor() { + return pingDivisor; + } + + public boolean isInstrumentationEnabled() { + return instrumentationEnabled; + } + + public int getInstrumentationInterval() { + return instrumentationInterval; + } + + public boolean isThreadHandoffFromTransport() { + return threadHandoffFromTransport; + } + + public boolean isRequeueOnInflightTimeout() { + return requeueOnInflightTimeout; + } + + public int getMaxProtocolMessageSize() { + return maxProtocolMessageSize; + } + + public boolean isWireLoggingEnabled() { + return wireLoggingEnabled; + } + + public int getActiveContextTimeout() { + return activeContextTimeout; + } + + public String getLogPattern() { + return logPattern; + } + + public int getMaxErrorRetries() { + return maxErrorRetries; + } + + public int getMaxErrorRetryTime() { + return maxErrorRetryTime; + } + + public boolean isReapReceivingMessages() { + return reapReceivingMessages; + } + + public int getStateProcessorThreadCount() { + return stateProcessorThreadCount; + } +} \ No newline at end of file diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnQueueAcceptException.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnQueueAcceptException.java new file mode 100644 index 00000000..b85212bd --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnQueueAcceptException.java @@ -0,0 +1,45 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.model; + +public class MqttsnQueueAcceptException extends Exception{ + public MqttsnQueueAcceptException() { + } + + public MqttsnQueueAcceptException(String message) { + super(message); + } + + public MqttsnQueueAcceptException(String message, Throwable cause) { + super(message, cause); + } + + public MqttsnQueueAcceptException(Throwable cause) { + super(cause); + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnSessionState.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnSessionState.java new file mode 100644 index 00000000..2d2b4c80 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnSessionState.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.model; + +import java.util.Date; + +public class MqttsnSessionState implements IMqttsnSessionState { + + private IMqttsnContext context; + private MqttsnClientState state; + private Date lastSeen; + private int keepAlive; + private Date sessionStarted; + + public MqttsnSessionState(IMqttsnContext context, MqttsnClientState state){ + this.context = context; + sessionStarted = new Date(); + this.state = state; + } + + @Override + public IMqttsnContext getContext() { + return context; + } + + @Override + public MqttsnClientState getClientState() { + return state; + } + + public void setContext(IMqttsnContext context) { + this.context = context; + } + + public void setClientState(MqttsnClientState state) { + this.state = state; + } + + @Override + public Date getLastSeen() { + return lastSeen; + } + + public void setLastSeen(Date lastSeen) { + this.lastSeen = lastSeen; + } + + @Override + public int getKeepAlive() { + return keepAlive; + } + + public void setKeepAlive(int keepAlive) { + this.keepAlive = keepAlive; + } + + @Override + public Date getSessionStarted() { + return sessionStarted; + } + + public void setSessionStarted(Date sessionStarted) { + this.sessionStarted = sessionStarted; + } + + @Override + public String toString() { + return "MqttsnSessionState{" + + "context=" + context + + ", state=" + state + + ", lastSeen=" + lastSeen + + ", keepAlive=" + keepAlive + + ", sessionStarted=" + sessionStarted + + '}'; + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnWaitToken.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnWaitToken.java new file mode 100644 index 00000000..ecdf76d0 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/MqttsnWaitToken.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.model; + +import org.slj.mqtt.sn.spi.IMqttsnMessage; + +import java.util.UUID; + +public class MqttsnWaitToken { + + private volatile boolean error = false; + private volatile boolean complete = false; + private volatile QueuedPublishMessage queuedPublishMessage; + private volatile IMqttsnMessage message; + private volatile IMqttsnMessage responseMessage; + + public MqttsnWaitToken(QueuedPublishMessage queuedPublishMessage){ + this.queuedPublishMessage = queuedPublishMessage; + } + + public MqttsnWaitToken(IMqttsnMessage message){ + this.message = message; + } + + public IMqttsnMessage getMessage() { + return message; + } + + public void setMessage(IMqttsnMessage message) { + this.message = message; + } + + public IMqttsnMessage getResponseMessage() { + return responseMessage; + } + + public void setResponseMessage(IMqttsnMessage responseMessage) { + this.responseMessage = responseMessage; + } + + public static MqttsnWaitToken from(QueuedPublishMessage queuedPublishMessage){ + return new MqttsnWaitToken(queuedPublishMessage); + } + + public static MqttsnWaitToken from(IMqttsnMessage message){ + return new MqttsnWaitToken(message); + } + + public boolean isComplete() { + return complete; + } + + public boolean isError() { + return error; + } + + public void markError() { + complete = true; + error = true; + } + + public void markComplete() { + this.complete = true; + error = false; + } + + @Override + public String toString() { + return "MqttsnWaitToken{" + + "error=" + error + + ", complete=" + complete + + ", message=" + message + + ", responseMessage=" + responseMessage + + '}'; + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/QueuedPublishMessage.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/QueuedPublishMessage.java new file mode 100644 index 00000000..2262e081 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/QueuedPublishMessage.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.model; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Lightweight meta-data reference to a message which will reside in client queues. NOTE: the payload of + * the message itself NOR the topic specification are included, this can be obtained JIT from the + * appropriate registries so we dont duplicate data across many queues. + */ +public class QueuedPublishMessage implements Serializable, Comparable { + + private Date created; + private String topicPath; + private int grantedQoS; + private int retryCount; + private UUID messageId; + private boolean retained; + private transient MqttsnWaitToken token; + + public QueuedPublishMessage() { + } + + public QueuedPublishMessage(UUID messageId, String topicPath, int grantedQoS) { + this.created = new Date(); + this.messageId = messageId; + this.topicPath = topicPath; + this.grantedQoS = grantedQoS; + this.retryCount = 0; + } + + public UUID getMessageId() { + return messageId; + } + + public void setMessageId(UUID messageId) { + this.messageId = messageId; + } + + public boolean getRetained() { + return retained; + } + + public void setRetained(boolean retained) { + this.retained = retained; + } + + public int getRetryCount() { + return retryCount; + } + + public void incrementRetry(){ + retryCount++; + } + + public String getTopicPath() { + return topicPath; + } + + public void setTopicPath(String topicPath) { + this.topicPath = topicPath; + } + + public int getGrantedQoS() { + return grantedQoS; + } + + public void setGrantedQoS(int grantedQoS) { + this.grantedQoS = grantedQoS; + } + + public void setRetryCount(int retryCount) { + this.retryCount = retryCount; + } + + public MqttsnWaitToken getToken() { + return token; + } + + public void setToken(MqttsnWaitToken token) { + this.token = token; + } + + @Override + public String toString() { + return "QueuedPublishMessage{" + + "created=" + created + + ", topicPath='" + topicPath + '\'' + + ", grantedQoS=" + grantedQoS + + ", retryCount=" + retryCount + + ", messageId=" + messageId + + ", retained=" + retained + + '}'; + } + + public Date getCreated() { + return created; + } + + @Override + public int compareTo(Object o) { + if(o instanceof QueuedPublishMessage){ + return created.compareTo(((QueuedPublishMessage)o).getCreated()); + } + return 0; + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/RequeueableInflightMessage.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/RequeueableInflightMessage.java new file mode 100644 index 00000000..3403099d --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/RequeueableInflightMessage.java @@ -0,0 +1,34 @@ +package org.slj.mqtt.sn.model; + +import org.slj.mqtt.sn.spi.IMqttsnMessage; + +public class RequeueableInflightMessage extends InflightMessage { + + QueuedPublishMessage queuedPublishMessage; + + public RequeueableInflightMessage(QueuedPublishMessage queuedPublishMessage, IMqttsnMessage message) { + super(message, DIRECTION.SENDING, queuedPublishMessage.getToken()); + if(queuedPublishMessage.getToken() != null) + queuedPublishMessage.getToken().setMessage(message); + + this.queuedPublishMessage = queuedPublishMessage; + } + + public QueuedPublishMessage getQueuedPublishMessage() { + return queuedPublishMessage; + } + + public void setQueuedPublishMessage(QueuedPublishMessage queuedPublishMessage) { + this.queuedPublishMessage = queuedPublishMessage; + } + + @Override + public String toString() { + return "RequeueableInflightMessage{" + + "token=" + token + + ", message=" + message + + ", time=" + time + + ", queuedPublishMessage=" + queuedPublishMessage + + '}'; + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/Subscription.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/Subscription.java new file mode 100644 index 00000000..dff55edb --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/Subscription.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.model; + +import org.slj.mqtt.sn.utils.TopicPath; + +import java.util.Objects; + +public class Subscription { + + TopicPath topicPath; + int QoS; + + public Subscription(){ + + } + + public Subscription(TopicPath topicPath){ + this.topicPath = topicPath; + } + + public Subscription(TopicPath topicPath, int qoS) { + this.topicPath = topicPath; + QoS = qoS; + } + + public TopicPath getTopicPath() { + return topicPath; + } + + public void setTopicPath(TopicPath topicPath) { + this.topicPath = topicPath; + } + + public int getQoS() { + return QoS; + } + + public void setQoS(int qoS) { + QoS = qoS; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Subscription that = (Subscription) o; + return Objects.equals(topicPath, that.topicPath); + } + + @Override + public int hashCode() { + return Objects.hash(topicPath); + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/TopicInfo.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/TopicInfo.java new file mode 100644 index 00000000..67279261 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/model/TopicInfo.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.model; + +import org.slj.mqtt.sn.MqttsnConstants; + +public class TopicInfo { + + // ** when subscribing to a wildcard topic, we return the wildcard alias response **/ + public static TopicInfo WILD = new TopicInfo(MqttsnConstants.TOPIC_TYPE.NORMAL, 0x00); + + MqttsnConstants.TOPIC_TYPE type; + int topicId; + String topicPath; + + public TopicInfo(MqttsnConstants.TOPIC_TYPE type, int topicId){ + this.type = type; + this.topicId = topicId; + } + + public TopicInfo(MqttsnConstants.TOPIC_TYPE type, String topicPath){ + this.type = type; + this.topicPath = topicPath; + } + + public MqttsnConstants.TOPIC_TYPE getType() { + return type; + } + + public void setType(MqttsnConstants.TOPIC_TYPE type) { + this.type = type; + } + + public int getTopicId() { + return topicId; + } + + public void setTopicId(int topicId) { + this.topicId = topicId; + } + + public String getTopicPath() { + return topicPath; + } + + public void setTopicPath(String topicPath) { + this.topicPath = topicPath; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("TopicInfo{"); + sb.append("type=").append(type); + sb.append(", topicId=").append(topicId); + sb.append(", topicPath='").append(topicPath).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/MqttsnTcpOptions.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/MqttsnTcpOptions.java new file mode 100644 index 00000000..1766cc5e --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/MqttsnTcpOptions.java @@ -0,0 +1,293 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.net; + +import javax.net.ServerSocketFactory; +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; + +public class MqttsnTcpOptions { + + /** + * Default connect timeout is 10000 milliseconds + */ + public static int DEFAULT_CONNECT_TIMEOUT = 10000; + + /** + * Default socket timeout is 0 (infinte) + */ + public static int DEFAULT_SO_TIMEOUT = 0; + + /** + * By default TCP keep alive is enabled + */ + public static boolean DEFAULT_TCP_KEEPALIVE_ENABLED = true; + + /** + * Default port is 1883 for gateways. + */ + public static int DEFAULT_PORT = 1883; + + /** + * Default secure port is 8883 for gateways. + */ + public static int DEFAULT_SECURE_PORT = 8883; + + /** + * Default localhost is null + */ + public static String DEFAULT_HOST = null; + + /** + * Default SSL cipher suites to enable + */ + public static String[] DEFAULT_SSL_CIPHER_SUITES = null; +// `new String[] { +// "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", +// "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", +// "TLS_RSA_WITH_AES_256_CBC_SHA", +// "TLS_RSA_WITH_AES_256_CBC_SHA256" +// }; + + /** + * Default SSL protocols + */ + public static String[] DEFAULT_SSL_PROTOCOLS = new String[] { + "TLSv1","TLSv1.1","TLSv1.2" + }; + + /** + * Default SSL protocols + */ + public static boolean DEFAULT_IS_SECURE = false; + + /** + * Default max concurrent client connections is 100 + */ + public static int DEFAULT_MAX_CLIENT_CONNECTIONS = 100; + + /** + * Default keystore password is unset + */ + public static String DEFAULT_KEYSTORE_PASSWORD = "password"; + + /** + * Default read buffer size is 1024 + */ + public static int DEFAULT_READ_BUFFER_SIZE = 1024; + + /** + * Default read buffer size is 1024 + */ + public static String DEFAULT_SSL_ALGORITHM = "TLS"; + + SocketFactory clientSocketFactory = SocketFactory.getDefault(); + ServerSocketFactory serverSocketFactory = ServerSocketFactory.getDefault(); + + String host = DEFAULT_HOST; + int port = DEFAULT_PORT; + int securePort = DEFAULT_SECURE_PORT; + + int maxClientConnections = DEFAULT_MAX_CLIENT_CONNECTIONS; + int connectTimeout = DEFAULT_CONNECT_TIMEOUT; + int soTimeout = DEFAULT_SO_TIMEOUT; + boolean tcpKeepAliveEnabled = DEFAULT_TCP_KEEPALIVE_ENABLED; + int readBufferSize = DEFAULT_READ_BUFFER_SIZE; + + //-- SSL stuff + boolean secure = DEFAULT_IS_SECURE; + String[] sslProtocols = DEFAULT_SSL_PROTOCOLS; + String[] cipherSuites = DEFAULT_SSL_CIPHER_SUITES; + String keyStorePath = null; + String trustStorePath = null; + String keyStorePassword = DEFAULT_KEYSTORE_PASSWORD; + String trustStorePassword = DEFAULT_KEYSTORE_PASSWORD; + String sslAlgorithm = DEFAULT_SSL_ALGORITHM; + + public MqttsnTcpOptions withSecure(boolean secure){ + this.secure = secure; + return this; + } + + public MqttsnTcpOptions withSSLAlgorithm(String sslAlgorithm){ + this.sslAlgorithm = sslAlgorithm; + return this; + } + + public MqttsnTcpOptions withReadBufferSize(int readBufferSize){ + this.readBufferSize = readBufferSize; + return this; + } + + public MqttsnTcpOptions withKeystorePassword(String keyStorePassword){ + this.keyStorePassword = keyStorePassword; + return this; + } + + public MqttsnTcpOptions withTruststorePassword(String trustStorePassword){ + this.trustStorePassword = trustStorePassword; + return this; + } + + public MqttsnTcpOptions withTruststorePath(String trustStorePath){ + this.trustStorePath = trustStorePath; + return this; + } + + public MqttsnTcpOptions withKeystorePath(String keyStorePath){ + this.keyStorePath = keyStorePath; + return this; + } + + public MqttsnTcpOptions withMaxClientConnections(int maxClientConnections){ + this.maxClientConnections = maxClientConnections; + return this; + } + + public MqttsnTcpOptions withSSLProtocols(String[] sslProtocols){ + this.sslProtocols = sslProtocols; + return this; + } + + public MqttsnTcpOptions withCipherSuites(String[] cipherSuites){ + this.cipherSuites = cipherSuites; + return this; + } + + public MqttsnTcpOptions withTcpKeepAliveEnabled(boolean keepAliveEnabled){ + this.tcpKeepAliveEnabled = keepAliveEnabled; + return this; + } + + public MqttsnTcpOptions withSoTimeout(int soTimeout){ + this.soTimeout = soTimeout; + return this; + } + + public MqttsnTcpOptions withConnectTimeout(int connectTimeout){ + this.connectTimeout = connectTimeout; + return this; + } + + public MqttsnTcpOptions withHost(String host){ + this.host = host; + return this; + } + + public MqttsnTcpOptions withPort(int port){ + this.port = port; + return this; + } + + public MqttsnTcpOptions withSecurePort(int securePort){ + this.securePort = securePort; + return this; + } + + public MqttsnTcpOptions withServerSocketFactory(ServerSocketFactory serverSocketFactory){ + this.serverSocketFactory = serverSocketFactory; + return this; + } + + public MqttsnTcpOptions withSocketFactory(SocketFactory clientSocketFactory){ + this.clientSocketFactory = clientSocketFactory; + return this; + } + + public int getConnectTimeout() { + return connectTimeout; + } + + public int getSoTimeout() { + return soTimeout; + } + + public boolean isTcpKeepAliveEnabled() { + return tcpKeepAliveEnabled; + } + + public String[] getSslProtocols() { + return sslProtocols; + } + + public String[] getCipherSuites() { + return cipherSuites; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public boolean isSecure() { + return secure; + } + + public int getMaxClientConnections() { + return maxClientConnections; + } + + public SocketFactory getClientSocketFactory() { + return clientSocketFactory; + } + + public ServerSocketFactory getServerSocketFactory() { + return serverSocketFactory; + } + + public String getKeyStorePassword() { + return keyStorePassword; + } + + public int getReadBufferSize() { + return readBufferSize; + } + + public int getSecurePort() { + return securePort; + } + + public String getKeyStorePath() { + return keyStorePath; + } + + public String getTrustStorePath() { + return trustStorePath; + } + + public String getTrustStorePassword() { + return trustStorePassword; + } + + public String getSslAlgorithm() { + return sslAlgorithm; + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/MqttsnTcpTransport.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/MqttsnTcpTransport.java new file mode 100644 index 00000000..a3b96871 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/MqttsnTcpTransport.java @@ -0,0 +1,561 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.net; + +import org.slj.mqtt.sn.impl.AbstractMqttsnTransport; +import org.slj.mqtt.sn.model.INetworkContext; +import org.slj.mqtt.sn.spi.IMqttsnMessage; +import org.slj.mqtt.sn.spi.IMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.spi.MqttsnException; +import org.slj.mqtt.sn.spi.NetworkRegistryException; + +import javax.net.ssl.*; +import java.io.*; +import java.net.*; +import java.nio.ByteBuffer; +import java.security.*; +import java.security.cert.CertificateException; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; + +/** + * TCP IP implementation support. Can be run with SSL (TLS) enabled for secure communication. + * Supports running in both client and server mode. + */ +public class MqttsnTcpTransport + extends AbstractMqttsnTransport { + + protected final MqttsnTcpOptions options; + protected boolean clientMode = false; + + static AtomicInteger connectionCount = new AtomicInteger(0); + private final Object monitor = new Object(); + protected Handler clientHandler; + protected Server server; + + public MqttsnTcpTransport(MqttsnTcpOptions options, boolean clientMode) { + this.options = options; + this.clientMode = clientMode; + } + + @Override + public synchronized void start(IMqttsnRuntimeRegistry runtime) throws MqttsnException { + try { + super.start(runtime); + running = false; + if(clientMode){ + logger.log(Level.INFO, String.format("running in client mode, establishing tcp connection...")); + connectClient(); + } else { + logger.log(Level.INFO, String.format("running in server mode, establishing tcp acceptors...")); + startServer(); + } + running = true; + synchronized (monitor){ + monitor.notifyAll(); + } + } catch(Exception e){ + running = false; + throw new MqttsnException(e); + } + } + + @Override + public synchronized void stop() throws MqttsnException { + super.stop(); + try { + if(clientMode){ + logger.log(Level.INFO, String.format("closing in client mode, closing tcp connection(s)...")); + try { + if(clientHandler != null){ + clientHandler.close(); + } + } finally { + clientHandler = null; + } + } else { + logger.log(Level.INFO, String.format("closing in server mode, closing tcp connection(s)...")); + try { + if(server != null){ + server.close(); + } + } finally { + server = null; + } + } + } catch(Exception e){ + throw new MqttsnException(e); + } + } + + @Override + protected void writeToTransport(INetworkContext context, ByteBuffer data) throws MqttsnException { + try { + if(clientMode){ + if(clientHandler == null){ + //-- in edge cases, we may get called by other services when still connecting.. + //-- so defend against that + synchronized (monitor){ + logger.log(Level.WARNING, "waiting for TCP stack to come up..."); + monitor.wait(options.getConnectTimeout() + 1000); + } + } + if(clientHandler != null){ + clientHandler.write(drain(data)); + } + + } else { + if(server != null){ + server.write(context, drain(data)); + } + } + } catch(IOException | InterruptedException e){ + throw new MqttsnException("error writing to connection;", e); + } + } + + protected SSLContext initSSLContext() + throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, UnrecoverableKeyException, KeyManagementException { + + String keyStorePath = options.getKeyStorePath(); + String trustStorePath = options.getTrustStorePath(); + logger.log(Level.INFO, String.format("initting SSL context with keystore [%s], truststore [%s]", keyStorePath, trustStorePath)); + + //-- keystore + KeyStore keyStore = null; + KeyManager[] keyManagers = null; + if(keyStorePath != null){ + File f = new File(keyStorePath); + if(!f.exists() || !f.canRead()) + throw new KeyStoreException("unable to read keyStore " + keyStorePath); + + keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + KeyManagerFactory kmf = KeyManagerFactory + .getInstance(KeyManagerFactory.getDefaultAlgorithm()); + + try(InputStream keyStoreData = new FileInputStream(f)) { + char[] password = options.getKeyStorePassword().toCharArray(); + keyStore.load(keyStoreData, password); + Enumeration aliases = keyStore.aliases(); + while(aliases.hasMoreElements()){ + String el = aliases.nextElement(); + logger.log(Level.INFO, String.format("keystore contains alias [%s]", el)); + } + kmf.init(keyStore, password); + keyManagers = kmf.getKeyManagers(); + } + } + + TrustManager[] trustManagers = null; + if(trustStorePath == null){ + logger.log(Level.WARNING, "!! ssl operating in trust-all mode, do not use this in production, nominate a trust store !!"); + TrustManager trustAllCerts = new X509TrustManager() { + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { + } + public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { + } + }; + trustManagers = new TrustManager[] { trustAllCerts }; + } else { + File f = new File(trustStorePath); + if(!f.exists() || !f.canRead()) + throw new KeyStoreException("unable to read trustStore " + trustStorePath); + + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + try(InputStream trustStoreData = new FileInputStream(f)) { + char[] password = options.getTrustStorePassword().toCharArray(); + trustStore.load(trustStoreData, password); + Enumeration aliases = keyStore.aliases(); + while(aliases.hasMoreElements()){ + String el = aliases.nextElement(); + logger.log(Level.INFO, String.format("truststore contains alias [%s]", el)); + } + TrustManagerFactory tmf = TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keyStore); + trustManagers = tmf.getTrustManagers(); + } + } + SSLContext ctx = SSLContext.getInstance(options.getSslAlgorithm()); + logger.log(Level.INFO, String.format("ssl initialised with algo [%s]", ctx.getProtocol())); + logger.log(Level.INFO, String.format("ssl initialised with JCSE provider [%s]", ctx.getProvider())); + logger.log(Level.INFO, String.format("ssl initialised with [%s] key manager(s)", keyManagers == null ? null : keyManagers.length)); + logger.log(Level.INFO, String.format("ssl initialised with [%s] trust manager(s)", trustManagers == null ? null : trustManagers.length)); + ctx.init(keyManagers, trustManagers, + SecureRandom.getInstanceStrong()); + return ctx; + + } + + protected void connectClient() throws MqttsnException { + try { + //-- check we have a valid network location to connect to + Optional remoteAddress = registry.getNetworkRegistry().first(); + if(!remoteAddress.isPresent()) throw new MqttsnException("need a remote location to connect to found "); + INetworkContext remoteContext = remoteAddress.get(); + + //-- create and bind local socket + if(clientHandler == null){ + synchronized (this){ + if(clientHandler == null){ + Socket clientSocket; + if(options.isSecure()){ + clientSocket = initSSLContext().getSocketFactory().createSocket(); + SSLSocket ssl = (SSLSocket) clientSocket; + if(options.getSslProtocols() != null) { + logger.log(Level.INFO, String.format("starting client-ssl with protocols [%s]", + Arrays.toString(options.getSslProtocols()))); + ssl.setEnabledProtocols(options.getSslProtocols()); + } + if(options.getCipherSuites() != null){ + logger.log(Level.INFO, String.format("starting client-ssl with cipher suites [%s]", + Arrays.toString(options.getCipherSuites()))); + ssl.setEnabledCipherSuites(options.getCipherSuites()); + } + + logger.log(Level.INFO, String.format("client-ssl enabled protocols [%s]", + Arrays.toString(ssl.getEnabledProtocols()))); + logger.log(Level.INFO, String.format("client-ssl enabled cipher suites [%s]", + Arrays.toString(ssl.getEnabledCipherSuites()))); + + } else { + clientSocket = options.getClientSocketFactory().createSocket(); + } + + //bind to the LOCAL address if specified, else bind to the OS default + InetSocketAddress localAddress = null; + if(options.getHost() != null){ + localAddress = new InetSocketAddress(options.getHost(), options.getPort()); + } + + clientSocket.bind(localAddress); + clientSocket.setSoTimeout(options.getSoTimeout()); + clientSocket.setKeepAlive(options.isTcpKeepAliveEnabled()); + + //-- if bound locally, connect to the remote if specified (running as a client) + if(clientSocket.isBound()){ + InetSocketAddress r = + new InetSocketAddress(remoteContext.getNetworkAddress().getHostAddress(), + remoteContext.getNetworkAddress().getPort()); + logger.log(Level.INFO, String.format("connecting client socket to [%s] -> [%s]", + remoteContext.getNetworkAddress().getHostAddress(), remoteContext.getNetworkAddress().getPort())); + clientSocket.connect(r, options.getConnectTimeout()); + } + + //-- need to be connected before handshake + if(clientSocket instanceof SSLSocket && + options.isSecure()){ + ((SSLSocket) clientSocket).startHandshake(); + } + + if(clientSocket.isBound() && clientSocket.isConnected()){ + clientHandler = new Handler(remoteContext, clientSocket, null); + clientHandler.start(); + } else { + logger.log(Level.SEVERE, "could not bind and connect socket to host, finished."); + } + } + } + } + } catch(Exception e){ + throw new MqttsnException(e); + } + } + + protected void startServer() + throws IOException, KeyStoreException, NoSuchAlgorithmException, KeyManagementException, + CertificateException, UnrecoverableKeyException { + if(server == null){ + synchronized (this) { + if (server == null) { + ServerSocket socket = null; + if(options.isSecure()){ + logger.log(Level.INFO, String.format("running in secure mode, tcp with TLS...")); + socket = initSSLContext().getServerSocketFactory().createServerSocket(options.getSecurePort()); + + SSLServerSocket ssl = (SSLServerSocket) socket; + if(options.getSslProtocols() != null) { + logger.log(Level.INFO, String.format("starting server-ssl with protocols [%s]", Arrays.toString(options.getSslProtocols()))); + ssl.setEnabledProtocols(options.getSslProtocols()); + } + if(options.getCipherSuites() != null){ + logger.log(Level.INFO, String.format("starting server-ssl with cipher suites [%s]", Arrays.toString(options.getCipherSuites()))); + ssl.setEnabledCipherSuites(options.getCipherSuites()); + } + + logger.log(Level.INFO, String.format("server-ssl enabled protocols [%s]", Arrays.toString(ssl.getEnabledProtocols()))); + logger.log(Level.INFO, String.format("server-ssl enabled cipher suites [%s]", Arrays.toString(ssl.getEnabledCipherSuites()))); + + } else { + socket = options.getServerSocketFactory().createServerSocket(options.getPort()); + } + server = new Server(socket); + server.start(); + } + } + } + } + + private class Server extends Thread implements ClosedListener{ + + private final ServerSocket serverSocket; + private Map connections = + Collections.synchronizedMap(new HashMap<>()); + + public Server(ServerSocket serverSocket){ + this.serverSocket = serverSocket; + setPriority(Thread.MIN_PRIORITY + 1); + setDaemon(false); + setName("mqtt-sn-tcp-server"); + } + + public void write(INetworkContext context, byte[] data) throws IOException { + Handler handler = connections.get(context); + if(handler == null) throw new IOException("no connected handler for context"); + handler.write(data); + } + + public void close(){ + if(connections != null && !connections.isEmpty()){ + connections.values().stream().forEach(c -> { + try { + c.close(); + } catch(Exception e){ + logger.log(Level.SEVERE, "error closing connection", e); + } + }); + } + } + + public void closed(Handler handler){ + if(connections != null && handler != null) + connections.remove(handler.descriptor.context); + } + + public void run(){ + logger.log(Level.INFO, String.format("starting TCP listener, accepting [%s] connections on port [%s]", + options.getMaxClientConnections(), serverSocket.getLocalPort())); + while(running){ + try { + Socket socket = serverSocket.accept(); + if(connections.size() >= options.getMaxClientConnections()){ + logger.log(Level.WARNING, "max connection limit reached, disconnecting socket"); + socket.close(); + + } else { + SocketAddress address = socket.getRemoteSocketAddress(); + logger.log(Level.INFO, + String.format("new connection accepted from [%s]", address)); + + NetworkAddress networkAddress = NetworkAddress.from((InetSocketAddress) address); + INetworkContext context = registry.getNetworkRegistry().getContext(networkAddress); + if(context == null){ + //-- if the network context does not exist in the registry, a new one is created by the factory - + //- NB: this is NOT auth, this is simply creating a context to which we can respond, auth can + //-- happen during the mqtt-sn context creation, at which point we can talk back to the device + //-- with error packets and the like + context = registry.getContextFactory().createInitialNetworkContext(networkAddress); + } + Handler handler = new Handler(context, socket, this); + connections.put(context, handler); + handler.start(); + } + + } catch (NetworkRegistryException | IOException | MqttsnException e){ + logger.log(Level.SEVERE, "error encountered accepting connection;", e); + } + } + } + } + + /** + * When running in server mode the handler will accept each connection lazily (not pre-pooled) + */ + class Handler extends Thread { + + private final SocketDescriptor descriptor; + private final ClosedListener listener; + + public Handler(INetworkContext context, Socket socket, ClosedListener listener) throws IOException { + this.descriptor = new SocketDescriptor(context, socket); + this.listener = listener; + setName("mqtt-sn-tcp-connection-" + connectionCount.incrementAndGet()); + setDaemon(false); + setPriority(Thread.NORM_PRIORITY); + } + + public void close() throws IOException { + descriptor.close(); + if(listener != null) listener.closed(this); + } + + public void write(byte[] data) throws IOException { + //-- TODO this needs buffering in + try { + if(descriptor.isOpen()){ + if(logger.isLoggable(Level.FINE)){ + logger.log(Level.FINE, String.format("writing [%s] bytes to output stream", data.length)); + } + descriptor.os.write(data); + descriptor.os.flush(); + } + } catch(SocketException e){ + try { + if(descriptor != null) { + connectionLost(descriptor.context, e); + } + } finally { + try { + descriptor.close(); + } catch (IOException ex) { + logger.log(Level.WARNING, "error closing socket descriptor;", e); + } + } + } + } + + public void run(){ + try { + logger.log(Level.INFO, String.format("starting socket handler for [%s]", descriptor)); + while(running && descriptor.isOpen()){ + int count; + int messageLength = 0; + int messageLengthRemaining = 0; + byte[] buff = new byte[options.getReadBufferSize()]; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + while ((count = descriptor.is.read(buff)) > 0) { + boolean isNewMessage = baos.size() == 0; + baos.write(buff, 0, count); + if(isNewMessage){ + messageLength = registry.getCodec().readMessageSize(baos.toByteArray()); + messageLengthRemaining = messageLength; + } + + messageLengthRemaining -= count; + + if (messageLengthRemaining == 0 && baos.size() == messageLength) { + if(logger.isLoggable(Level.FINE)){ + logger.log(Level.FINE, String.format("received [%s] bytes from socket for [%s], reset buffer", messageLength, descriptor)); + } + receiveFromTransport(descriptor.context, wrap(baos.toByteArray())); + messageLength = 0; + messageLengthRemaining = 0; + baos.reset(); + } + } + } + + if(count == 0 || count == -1) { + logger.log(Level.INFO, String.format("received [%s] bytes from socket handler (end of stream - EOF), ", count, descriptor)); + //throw here will cascade the error up to the runtime + throw new SocketException("EOF received"); + } + } + } + catch(SSLProtocolException | SocketException e){ + //-- socket close from underneath during reading + logger.log(Level.FINE, String.format("socket error [%s]", descriptor), e); + if(descriptor != null){ + if(!descriptor.closed){ + //it may have been that we asked for a stop and there was an issue closing the socket + //in which cast we didnt LOSE the connection.. only report lost if the + //service was meant to be running + connectionLost(descriptor.context, e); + } + } + } + catch(IOException e){ + throw new RuntimeException("error accepting data from client stream;",e); + } finally { + try { + descriptor.close(); + } catch (IOException e) { + logger.log(Level.WARNING, "error closing socket descriptor;", e); + } + } + } + } + + private class SocketDescriptor implements Closeable { + + private volatile boolean closed = false; + private INetworkContext context; + private Socket socket; + private InputStream is; + private OutputStream os; + + public SocketDescriptor(INetworkContext context, Socket socket) throws IOException { + this.context = context; + this.socket = socket; + if(socket.isClosed()) throw new IllegalArgumentException("cannot get a descriptor onto closed socket"); + is = socket.getInputStream(); + os = socket.getOutputStream(); + } + + public boolean isOpen(){ + return socket != null && socket.isConnected() && !socket.isClosed(); + } + + @Override + public String toString() { + return Objects.toString(context); + } + + @Override + public synchronized void close() throws IOException { + try { + if(!closed){ + int current = connectionCount.decrementAndGet(); + logger.log(Level.INFO, String.format("closing socket [%s], activeConnection(s) [%s]", this, current)); + closed = true; + try {is.close();} catch(Exception e){} + try {os.close();} catch(Exception e){} + try {socket.close();} catch(Exception e){} + } + } finally { + socket = null; + context = null; + os = null; + is = null; + } + } + } + + @Override + public void broadcast(IMqttsnMessage message) throws MqttsnException { + throw new UnsupportedOperationException("broadcast not supported on TCP"); + } +} + +interface ClosedListener { + void closed(MqttsnTcpTransport.Handler handler); +} \ No newline at end of file diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/MqttsnUdpOptions.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/MqttsnUdpOptions.java new file mode 100644 index 00000000..bd2cd059 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/MqttsnUdpOptions.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.net; + +/** + * Options to configure the behaviour of the UDP transport layer + */ +public class MqttsnUdpOptions { + + /** + * Default max tranmission unit for UDP transport + */ + public static int DEFAULT_MTU = 1024; + + /** + * Default host binds to wildcard (kernel assigned) + */ + public static String DEFAULT_LOCAL_BIND_INTERFACE = "0.0.0.0"; + + /** + * Default local port for client mode is 0 (any available local port) for clients. + */ + public static int DEFAULT_LOCAL_CLIENT_PORT = 0; + + /** + * Default local port is 2442 for gateways. + */ + public static int DEFAULT_LOCAL_PORT = 2442; + + /** + * Default local DTLS port is 2443 for gateways and unspecified (any available local port) for clients. + */ + public static int DEFAULT_SECURE_PORT = 2443; + + /** + * Default multicast port is 2224 + */ + public static int DEFAULT_BROADCAST_PORT = 2224; + + /** + * Default receive buffer size is 1024 + */ + public static int DEFAULT_RECEIVE_BUFFER_SIZE = 1024; + + /** + * Default receive buffer size is 1024 + */ + public static boolean DEFAULT_BIND_BROADCAST_LISTENER = false; + + String host = DEFAULT_LOCAL_BIND_INTERFACE; + int port = DEFAULT_LOCAL_PORT; + int mtu = DEFAULT_MTU; + int securePort = DEFAULT_SECURE_PORT; + int broadcastPort = DEFAULT_BROADCAST_PORT; + int receiveBuffer = DEFAULT_RECEIVE_BUFFER_SIZE; + boolean bindBroadcastListener = DEFAULT_BIND_BROADCAST_LISTENER; + + /** + * Max allowable tranmission unit + * + * @see {@link MqttsnUdpOptions#DEFAULT_MTU} for default gateway value + * + * @param mtu - Mtu for the medium + * @return this config + */ + public MqttsnUdpOptions withMtu(int mtu){ + this.mtu = mtu; + return this; + } + + /** + * Which local port to use for datagram messaging. When running in client mode, set 0 (default) which will mean + * any available local port will be used. + * + * @see {@link MqttsnUdpOptions#DEFAULT_LOCAL_PORT} for default gateway value + * @see {@link MqttsnUdpOptions#DEFAULT_LOCAL_CLIENT_PORT} for default client value + * + * @param port - Which local port to use for datagram messaging + * @return this config + */ + public MqttsnUdpOptions withPort(int port){ + this.port = port; + return this; + } + + /** + * For the most part, the datagram binds to the local wildcard address (0.0.0.0) which will be decided + * by the kernal. In certain implementations you can supply the host on which to bind here + * @param host - the host to bind the listening socket to + * @return this config + */ + public MqttsnUdpOptions withHost(String host){ + this.host = host; + return this; + } + + /** + * Which local port to use for secure datagram messaging. When running in client mode, set 0 (default) which will mean + * any available local port will be used. + * + * @see {@link MqttsnUdpOptions#DEFAULT_SECURE_PORT} for default gateway value + * @see {@link MqttsnUdpOptions#DEFAULT_LOCAL_CLIENT_PORT} for default client value + * + * @param port - Which local port to use for datagram messaging + * @return this config + */ + public MqttsnUdpOptions withSecurePort(int port){ + this.securePort = port; + return this; + } + + /** + * Set the size of the transport buffer used to receive messages + * + * @see {@link MqttsnUdpOptions#DEFAULT_RECEIVE_BUFFER_SIZE} + * + * @param receiveBuffer - Set the size of the transport buffer used to receive messages + * @return this config + */ + public MqttsnUdpOptions withReceiveBuffer(int receiveBuffer){ + this.receiveBuffer = receiveBuffer; + return this; + } + + /** + * Which local port to use for broadcast messaging + * + * @see {@link MqttsnUdpOptions#DEFAULT_BROADCAST_PORT} for default gateway + * @see {@link MqttsnUdpOptions#DEFAULT_LOCAL_CLIENT_PORT} for default client value + * + * @param multicastPort - Which local port to use for multicast messaging + * @return this config + */ + public MqttsnUdpOptions withBroadcastPort(int multicastPort){ + this.broadcastPort = broadcastPort; + return this; + } + + /** + * Should the runtime listen on the broadcast port for discovery messages + * + * @see {@link MqttsnUdpOptions#DEFAULT_BIND_BROADCAST_LISTENER} for default gateway + * + * @param broadcastListener - Should the runtime listen on the broadcast port for discovery messages + * @return this config + */ + public MqttsnUdpOptions withBindBroadcastListener(boolean broadcastListener){ + this.bindBroadcastListener = broadcastListener; + return this; + } + + public boolean getBindBroadcastListener() { + return bindBroadcastListener; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public int getReceiveBuffer() { + return receiveBuffer; + } + + public int getBroadcastPort(){ return this.broadcastPort; } + + public int getSecurePort() { + return securePort; + } + + public int getMtu() { return mtu; } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/MqttsnUdpTransport.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/MqttsnUdpTransport.java new file mode 100644 index 00000000..fa884811 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/MqttsnUdpTransport.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.net; + +import org.slj.mqtt.sn.impl.AbstractMqttsnUdpTransport; +import org.slj.mqtt.sn.model.INetworkContext; +import org.slj.mqtt.sn.spi.IMqttsnMessage; +import org.slj.mqtt.sn.spi.MqttsnException; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.logging.Level; + +/** + * Provides a transport over User Datagram Protocol (UDP). This implementation uses a receiver thread which binds + * onto a Socket in a tight loop blocking on receive with no socket timeout set (0). The receiver thread will simply hand packets + * off to the base receive method who will either pass to the thread pool for handling or handle blocking depending on the + * configuration of the runtime. + * + * The broadcast-receiver when activated runs on it own thread, listening on the broadcast port, updating the registry + * when new contexts are discovered. + */ +public class MqttsnUdpTransport extends AbstractMqttsnUdpTransport { + + private DatagramSocket socket; + private DatagramSocket broadcastSocket; + private Thread receiverThread; + private Thread broadcastThread; + protected boolean running = false; + + public MqttsnUdpTransport(MqttsnUdpOptions udpOptions){ + super(udpOptions); + } + + protected synchronized void bind() throws SocketException { + + running = true; + int bufferSize = options.getReceiveBuffer(); + socket = options.getPort() > 0 ? new DatagramSocket(options.getPort()) : new DatagramSocket(); + //-- by default we do not set SoTimeout (infinite) which will block until recieve + receiverThread = createDatagramServer("mqtt-sn-udp-receiver", bufferSize, socket); + if(options.getBindBroadcastListener() && registry.getOptions().isEnableDiscovery()) { + broadcastSocket = options.getBroadcastPort() > 0 ? new DatagramSocket(options.getBroadcastPort()) : new DatagramSocket(); + broadcastSocket.setBroadcast(true); + broadcastThread = createDatagramServer("mqtt-sn-udp-broadcast", bufferSize, broadcastSocket); + } + } + + protected Thread createDatagramServer(final String threadName, final int bufSize, final DatagramSocket socketIn){ + Thread thread = new Thread(() -> { + logger.log(Level.INFO, String.format("creating udp server [%s] bound to socket [%s] with buffer size [%s], running ? [%s]", threadName, socketIn.getLocalPort(), bufSize, running)); + byte[] buff = new byte[bufSize]; + while(running && !socketIn.isClosed()){ + try { + DatagramPacket p = new DatagramPacket(buff, buff.length); + socketIn.receive(p); + int length = p.getLength(); + + if(logger.isLoggable(Level.FINE)){ + logger.log(Level.FINE, "udp receive: length = " + length + ", offset = " + p.getOffset() + ", data = " + p.getData().length); + } + + NetworkAddress address = NetworkAddress.from(p.getPort(), p.getAddress().getHostAddress()); + INetworkContext context = registry.getNetworkRegistry().getContext(address); + if(context == null){ + //-- if the network context does not exist in the registry, a new one is created by the factory - + //- NB: this is NOT auth, this is simply creating a context to which we can respond, auth can + //-- happen during the mqtt-sn context creation, at which point we can talk back to the device + //-- with error packets and the like + context = registry.getContextFactory().createInitialNetworkContext(address); + } + context.setReceivePort(socket.getLocalPort()); + receiveDatagramInternal(context, p); + } catch(Throwable e){ + logger.log(Level.SEVERE, "encountered an error listening for traffic", e); + } finally { + buff = new byte[bufSize]; + } + } + }, threadName); + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY + 1); + thread.start(); + return thread; + } + + @Override + public void stop() throws MqttsnException { + super.stop(); + running = false; + socket = null; + broadcastSocket = null; + receiverThread = null; + broadcastThread = null; + } + + @Override + public void writeToTransport(INetworkContext context, ByteBuffer buffer) throws MqttsnException { + try { + byte[] payload = drain(buffer); + DatagramPacket packet = new DatagramPacket(payload, payload.length); + sendDatagramInternal(context, packet); + } catch(Exception e){ + throw new MqttsnException(e); + } + } + + protected void receiveDatagramInternal(INetworkContext context, DatagramPacket packet) throws Exception { + receiveFromTransport(context, wrap(packet.getData(), packet.getLength())); + } + + protected void sendDatagramInternal(INetworkContext context, DatagramPacket packet) throws Exception { + NetworkAddress address = context.getNetworkAddress(); + InetAddress inetAddress = InetAddress.getByName(address.getHostAddress()); + packet.setAddress(inetAddress); + packet.setPort(address.getPort()); + if(logger.isLoggable(Level.FINE)){ + logger.log(Level.FINE, String.format("sending [%s] byte Datagram (with stack) to [%s] -> [%s]", + packet.getLength(), address, address.getPort()), new Exception("trace")); + } else { + logger.log(Level.INFO, String.format("sending [%s] byte Datagram to [%s] -> [%s]", + packet.getLength(), address, address.getPort())); + } + + socket.send(packet); + } + + @Override + public void broadcast(IMqttsnMessage broadcastMessage) throws MqttsnException { + try { + byte[] arr = registry.getCodec().encode(broadcastMessage); + List broadcastAddresses = registry.getNetworkRegistry().getAllBroadcastAddresses(); + try (DatagramSocket socket = new DatagramSocket()){ + socket.setBroadcast(true); + for(InetAddress address : broadcastAddresses) { + logger.log(Level.FINE, String.format("broadcasting [%s] message to network interface [%s] -> [%s]", + broadcastMessage.getMessageName(), address, options.getBroadcastPort())); + DatagramPacket packet + = new DatagramPacket(arr, arr.length, address, options.getBroadcastPort()); + socket.send(packet); + } + } + } catch(Exception e){ + throw new MqttsnException(e); + } + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/NetworkAddress.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/NetworkAddress.java new file mode 100644 index 00000000..c2eb4a4a --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/NetworkAddress.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.net; + +import java.io.Serializable; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.util.Objects; + +/** + * A network address simply represents a host name or IP address and port combination. + * Represents a remote ipv4 or ipv6 location. Regardless of the supplied format, the address will + * be stored in its canonical form. + */ +public class NetworkAddress implements Serializable { + + private final String hostAddress; + private final int port; + + /** + * Create a new network address from the port and address supplied. + * @param port - The port on which the remote is bound + * @param hostAddress - A valid ipv4, ipv6 or host address. Where a name is supplied, + * an attempt will be made to eagerly resolve it so unknown hosts are derived eagerly. + * @throws UnknownHostException - no host could be found + */ + public NetworkAddress(int port, String hostAddress) throws UnknownHostException { + this.hostAddress = InetAddress.getByName(hostAddress).getHostAddress(); + if(port < 0 || port > 65535) throw new IllegalArgumentException("port must be in range 0 <= port <= 65535"); + this.port = port; + } + + public String getHostAddress() { + return hostAddress; + } + + public int getPort() { + return port; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NetworkAddress that = (NetworkAddress) o; + return port == that.port && + Objects.equals(hostAddress, that.hostAddress); + } + + @Override + public int hashCode() { + return Objects.hash(hostAddress, port); + } + + public InetSocketAddress toSocketAddress(){ + return InetSocketAddress.createUnresolved(hostAddress, port); + } + + public static NetworkAddress from(InetSocketAddress address) throws UnknownHostException { + return NetworkAddress.from(address.getPort(), address.getAddress().getHostAddress()); + } + + public static NetworkAddress from(int port, String hostAddress) throws UnknownHostException { + return new NetworkAddress(port, hostAddress); + } + + /** + * Convenience method to obtain a network address to local host loopback (127.0.0.1) on the port specified. + * @param port - the local port + * @return the NetworkAddress + */ + public static NetworkAddress localhost(int port) { + try { + return new NetworkAddress(port, "127.0.0.1"); + } catch(UnknownHostException e){ + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("SocketAddress ["); + sb.append("address='").append(hostAddress).append('\''); + sb.append(", port=").append(port); + sb.append(']'); + return sb.toString(); + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/NetworkAddressRegistry.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/NetworkAddressRegistry.java new file mode 100644 index 00000000..e2121184 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/NetworkAddressRegistry.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.net; + +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.INetworkContext; +import org.slj.mqtt.sn.spi.INetworkAddressRegistry; +import org.slj.mqtt.sn.spi.MqttsnRuntimeException; +import org.slj.mqtt.sn.spi.NetworkRegistryException; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class NetworkAddressRegistry implements INetworkAddressRegistry { + + static Logger logger = Logger.getLogger(NetworkAddressRegistry.class.getName()); + + final protected Map networkRegistry; + final protected Map mqttsnContextRegistry; + final protected Map networkContextRegistry; + + final private Object mutex = new Object(); + + public NetworkAddressRegistry(int initialCapacity){ + networkRegistry = Collections.synchronizedMap(new HashMap<>(initialCapacity)); + mqttsnContextRegistry = Collections.synchronizedMap(new HashMap<>(initialCapacity)); + networkContextRegistry = Collections.synchronizedMap(new HashMap<>(initialCapacity)); + } + + @Override + public INetworkContext getContext(NetworkAddress address) throws NetworkRegistryException { + INetworkContext context = networkRegistry.get(address); + logger.log(Level.FINE, String.format("getting network context from RAM registry by address [%s] -> [%s]", address, context)); + return context; + } + + @Override + public INetworkContext getContext(IMqttsnContext sessionContext) { + INetworkContext context = mqttsnContextRegistry.get(sessionContext); + if(context == null) + throw new MqttsnRuntimeException("unable to get network route for session " + sessionContext); + logger.log(Level.FINE, String.format("getting network context from RAM registry by session [%s] -> [%s]", sessionContext, context)); + return context; + } + + @Override + public IMqttsnContext getSessionContext(INetworkContext networkContext){ + IMqttsnContext context = networkContextRegistry.get(networkContext); + if(context == null) + throw new MqttsnRuntimeException("unable to get session context for network route " + networkContext); + logger.log(Level.FINE, String.format("getting session context from RAM registry network route [%s] -> [%s]", networkContext, context)); + return context; + } + + @Override + public Optional first() throws NetworkRegistryException { + Iterator itr = networkRegistry.keySet().iterator(); + synchronized (networkRegistry){ + while(itr.hasNext()){ + NetworkAddress address = itr.next(); + INetworkContext c = networkRegistry.get(address); + return Optional.of(c); + } + } + return Optional.empty(); + } + + @Override + public Optional findForClientId(String clientId) { + if(clientId == null) return null; + synchronized (mqttsnContextRegistry){ + return mqttsnContextRegistry.keySet().stream(). + filter(s -> s.getId() != null). + filter(s -> s.getId().equals(clientId)).findFirst(); + } + } + + @Override + public void putContext(INetworkContext context) { + networkRegistry.put(context.getNetworkAddress(), context); + logger.log(Level.INFO, String.format("adding network context to RAM registry - [%s]", context)); + synchronized(mutex){ + mutex.notifyAll(); + } + } + + @Override + public void bindContexts(INetworkContext context, IMqttsnContext sessionContext) { + logger.log(Level.INFO, String.format("binding network to session contexts - [%s] -> [%s]", context, sessionContext)); + mqttsnContextRegistry.put(sessionContext, context); + networkContextRegistry.put(context, sessionContext); + putContext(context); + } + + @Override + public boolean hasBoundSessionContext(INetworkContext context){ + IMqttsnContext c = networkContextRegistry.get(context); + return c != null; + } + + @Override + public boolean removeExistingClientId(String clientId){ + Iterator itr = mqttsnContextRegistry.keySet().iterator(); + synchronized (mqttsnContextRegistry) { + while (itr.hasNext()) { + IMqttsnContext m = itr.next(); + if(m.getId().equals(clientId)){ + INetworkContext c = mqttsnContextRegistry.get(m); + itr.remove(); + networkRegistry.remove(c.getNetworkAddress()); + networkContextRegistry.remove(c); + logger.log(Level.INFO, String.format("removing network,session & address from RAM registry - [%s]", clientId)); + return true; + } + } + } + + return false; + } + + public Optional waitForContext(int time, TimeUnit unit) throws NetworkRegistryException, InterruptedException { + synchronized(mutex){ + try { + while(networkRegistry.isEmpty()){ + mutex.wait(TimeUnit.MILLISECONDS.convert(time, unit)); + } + return first(); + } catch(InterruptedException e){ + Thread.currentThread().interrupt(); + throw e; + } + } + } + + public Iterator iterator() throws NetworkRegistryException { + return networkRegistry.values().iterator(); + } + + public List getAllBroadcastAddresses() throws NetworkRegistryException { + try { + List l = new ArrayList<>(); + Enumeration interfaces + = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface networkInterface = interfaces.nextElement(); + if (networkInterface.isLoopback() || + !networkInterface.isUp()) { + continue; + } + networkInterface.getInterfaceAddresses().stream() + .map(a -> a.getBroadcast()) + .filter(Objects::nonNull) + .forEach(l::add); + } + return l; + } catch(SocketException e){ + throw new NetworkRegistryException(e); + } + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/NetworkContext.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/NetworkContext.java new file mode 100644 index 00000000..2440e995 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/net/NetworkContext.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.net; + +import org.slj.mqtt.sn.model.AbstractContextObject; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.INetworkContext; + +public class NetworkContext extends AbstractContextObject implements INetworkContext { + + protected NetworkAddress networkAddress; + protected int receivePort; + + public NetworkContext(){} + + public NetworkContext(NetworkAddress networkAddress){ + this.networkAddress = networkAddress; + } + + public int getReceivePort() { + return receivePort; + } + + public void setReceivePort(int receivePort) { + this.receivePort = receivePort; + } + + @Override + public NetworkAddress getNetworkAddress() { + return networkAddress; + } + + public void setNetworkAddress(NetworkAddress networkAddress) { + this.networkAddress = networkAddress; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NetworkContext that = (NetworkContext) o; + return networkAddress.equals(that.networkAddress); + } + + @Override + public int hashCode() { + return networkAddress.hashCode(); + } + + @Override + public String toString() { + return "NetworkContext{" + + "networkAddress=" + networkAddress + + ", receivePort=" + receivePort + + "," + super.toString() + + '}'; + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnConnectionStateListener.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnConnectionStateListener.java new file mode 100644 index 00000000..aab1ba2b --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnConnectionStateListener.java @@ -0,0 +1,85 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.IMqttsnContext; + +/** + * Bind in a listener to be notified of CONNECTION events from the various sub-systems + */ +public interface IMqttsnConnectionStateListener { + + /** + * The context has successfully exchanged CONNECT messages with the remote. + * Generated by the message handler + * @param context - the origin context + */ + void notifyConnected(IMqttsnContext context); + + /** + * An unsolicited DISCONNECT message was received from the context + * Generated by the message handler + * @param context - the origin context + */ + void notifyRemoteDisconnect(IMqttsnContext context); + + /** + * Where applicable, the runtime has not exchanged any active messages (CONNECT, PUBLISH, SUBSCRIBE, UNSUBSCRIBE) + * with the remote. + * + * NB: This is different from keepAlive, and is a timing around ACTIVE rather than PASSIVE messages. ie. excluding PING + * + * Generated by the message state service + * + * @param context - the origin context + */ + void notifyActiveTimeout(IMqttsnContext context); + + /** + * An error occurred in the local runtime which caused an unsolicited DISCONNECT operation to be sent + * to the remote + * + * NB: this is only called for DISCONNECT operations not created by the application + * + * Generated by the message handler + * + * @param context - the origin context + * @param t - the exception that caused the local error + */ + void notifyLocalDisconnect(IMqttsnContext context, Throwable t); + + /** + * When stateful connections are held by the transport layer (ie. TCP sockets) + * when connections are lost (e.g. socket timeout on read, EOF received, write IO) + * the method can propogate the event up to the application + * @param context - the origin context + * @param t - the exception that caused the connection loss + */ + void notifyConnectionLost(IMqttsnContext context, Throwable t); + +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnContextFactory.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnContextFactory.java new file mode 100644 index 00000000..c6e3bc7a --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnContextFactory.java @@ -0,0 +1,54 @@ +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.INetworkContext; +import org.slj.mqtt.sn.model.MqttsnContext; +import org.slj.mqtt.sn.net.NetworkAddress; +import org.slj.mqtt.sn.net.NetworkContext; + +/** + * A context factory deals with the initial construction of the context objects which identity + * the remote connection to the application. There are 2 types of context; a {@link NetworkContext} + * and a {@link MqttsnContext}. The network context identifies where (the network location) the identity + * resides and the mqttsn-context identifies who the context is (generally this is the CliendId or GatewayId of + * the connected resource). + * + * A {@link NetworkContext} can exist in isolation without an associated {@link MqttsnContext}, during a CONNECT attempt + * (when the context has yet to be established), or during a failed CONNECTion. An application context cannot exist without + * a network context. + * + * You can provide your own implementation, if you wish to wrap or provide your own extending context implementation + * to wrap custom User objects, for example. + * + */ +public interface IMqttsnContextFactory { + + /** + * When no network context can be found in the registry from the associated {@link NetworkAddress}, + * the factrory is called to create a new instance from the address supplied. + * @param address - the source address from which traffic has been received. + * @return - the new instance of a network context bound to the address supplied + * @throws MqttsnException - an error has occurred + */ + INetworkContext createInitialNetworkContext(NetworkAddress address) throws MqttsnException; + + /** + * No application existed for the network context OR a new clientId was detected, so we + * should create a new application context pinned to the network context supplied. + * @param context - The source network context + * @param clientId - The clientId which was supplied by the CONNECT packet + * @return the new instance of the application context coupled to the network context + * @throws MqttsnSecurityException - The supplied clientId was not allowed on the gateway + */ + IMqttsnContext createInitialApplicationContext(INetworkContext context, String clientId) throws MqttsnSecurityException; + + /** + * No application existed for the network context OR a new clientId was detected, so we + * should create a new application context pinned to the network context supplied. + * @param context - The source network context + * @return the new instance of the application context coupled to the network context + * @throws MqttsnSecurityException - The supplied clientId was not allowed on the gateway + */ + IMqttsnContext createTemporaryApplicationContext(INetworkContext context) throws MqttsnSecurityException; + +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnInstrumentationProvider.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnInstrumentationProvider.java new file mode 100644 index 00000000..b19a75d1 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnInstrumentationProvider.java @@ -0,0 +1,15 @@ +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.utils.StringTable; + +/** + * When called, the provider is a source of instrumentation which will be output by the runtime + */ +public interface IMqttsnInstrumentationProvider { + + /** + * Provide a snapshot at the point of invocation + * @return A {@link StringTable} containing details of the current runtime + */ + StringTable provideInstrumentation(); +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageHandler.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageHandler.java new file mode 100644 index 00000000..41d70b68 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageHandler.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.INetworkContext; + +/** + * The message handler is delegated to by the transport layer and its job is to process + * inbound messages and marshall into other controllers to manage state lifecycle, authentication, permission + * etc. + * + * It is directly responsible for creating response messages and sending them back to the transport layer + */ +public interface IMqttsnMessageHandler extends IMqttsnService{ + + + /** + * Determine if a network location is considered valid for a publish -1 + * @param context - the network context from where a message originated + * @return - true if we consider the context authorised, false otherwise + * @throws MqttsnException - an error occurred + */ + boolean temporaryAuthorizeContext(INetworkContext context) + throws MqttsnException; + + + /** + * Determine if a network location and clientId are considered authorised + * @param context - the network context from where a message originated + * @param clientId - the clientId passed in during a CONNECT procedure + * @return - true if we consider the context authorised, false otherwise + * @throws MqttsnException - an error occurred + */ + boolean authorizeContext(INetworkContext context, String clientId) + throws MqttsnException; + + void receiveMessage(IMqttsnContext context, IMqttsnMessage message) + throws MqttsnException; + + boolean canHandle(IMqttsnContext context, IMqttsnMessage message) + throws MqttsnException; + + boolean validResponse(IMqttsnMessage request, IMqttsnMessage response); + + boolean requiresResponse(IMqttsnMessage message); + + boolean isTerminalMessage(IMqttsnMessage message); + + boolean isPartOfOriginatingMessage(IMqttsnMessage message); + +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageQueue.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageQueue.java new file mode 100644 index 00000000..efdd5449 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageQueue.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.MqttsnQueueAcceptException; +import org.slj.mqtt.sn.model.MqttsnWaitToken; +import org.slj.mqtt.sn.model.QueuedPublishMessage; + +import java.util.Iterator; +import java.util.List; + +/** + * Queue implementation to store messages destined to and from gateways and clients. Queues will be flushed acccording + * to the session semantics defined during CONNECT. + * + * Ideally the queue should be implemented to support FIFO where possible. + */ +public interface IMqttsnMessageQueue extends IMqttsnRegistry { + + /** + * List the contexts which are present within this queue. Each context represents a client or gateway Queue of which + * there will be many when run in gateway mode. + * + * @return - An iterator over the contexts for which a queue is presently held + * @throws MqttsnException - an error occurred + */ + Iterator listContexts() throws MqttsnException; + + /** + * Return the size of the queue for a given context + * @param context - the context whose queue youd like to query + * @return - the size of the queue for a given context + * @throws MqttsnException - an error occurred + */ + int size(IMqttsnContext context) throws MqttsnException; + + /** + * Offer the queue or a context a new message to add to the tail. + * @param context - the context whose queue youd like to append + * @param message - the message metadata to queue + * @return - token if the message was added + * @throws MqttsnException - an error occurred, most likely the queue was full + */ + MqttsnWaitToken offer(IMqttsnContext context, QueuedPublishMessage message) throws MqttsnException, MqttsnQueueAcceptException; + + /** + * Pop a message from the head of the queue. This removes and returns the message at the Head. + * @param context - the context whose queue youd like to query + * @return the message from the head of the queue or NULL if the queue is empty + * @throws MqttsnException - an error occurred + */ + QueuedPublishMessage pop(IMqttsnContext context) throws MqttsnException; + + /** + * Peek at the message on the head of the queue without removing it + * @param context - the context whose queue youd like to query + * @return the message from the head of the queue or NULL if the queue is empty + * @throws MqttsnException - an error occurred + */ + QueuedPublishMessage peek(IMqttsnContext context) throws MqttsnException; + +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageQueueProcessor.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageQueueProcessor.java new file mode 100644 index 00000000..f0b2e008 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageQueueProcessor.java @@ -0,0 +1,28 @@ +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.IMqttsnContext; + +/** + * The job of the queue processor is to (when requested) interact with a remote contexts' queue, processing + * the next message from the HEAD of the queue, handling any topic registration, session state, marking + * messages inflight and finally returning an indicator as to what should happen when the processing of + * the next message is complete. Upon dealing with the next message, whether successful or not, the processor + * needs to return an indiction; + * + * REMOVE_PROCESS - The queue is empty and the context no longer needs further processing + * BACKOFF_PROCESS - The queue is not empty, come back after a backend to try again. Repeating this return type for the same context + * will yield an exponential backoff + * REPROCESS (continue) - The queue is not empty, (where possible) call me back immediately to process again + * + */ +public interface IMqttsnMessageQueueProcessor + extends IMqttsnService { + + enum RESULT { + REMOVE_PROCESS, + BACKOFF_PROCESS, + REPROCESS + } + + RESULT process(IMqttsnContext context) throws MqttsnException ; +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageRegistry.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageRegistry.java new file mode 100644 index 00000000..13d42992 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageRegistry.java @@ -0,0 +1,25 @@ +package org.slj.mqtt.sn.spi; + +import java.util.Date; +import java.util.UUID; + +/** + * The message registry is a normalised view of transiting messages, it context the raw payload of publish operations + * so light weight references to the payload can exist in multiple storage systems without duplication of data. + * For example, when running in gateway mode, the same message my reside in queues for numerous devices which are + * in different connection states. We should not store payloads N times in this case. + * + * The lookup is a simple UUID -> byte[] relationship. It is up to the registry implementation to decide how to store + * this data. + * + */ +public interface IMqttsnMessageRegistry extends IMqttsnRegistry{ + + void tidy() throws MqttsnException ; + + UUID add(byte[] data, boolean removeAfterRead) throws MqttsnException ; + + UUID add(byte[] data, Date expires) throws MqttsnException; + + byte[] get(UUID messageId) throws MqttsnException; +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageStateService.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageStateService.java new file mode 100644 index 00000000..4469b0b8 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnMessageStateService.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.*; + +import java.util.Date; +import java.util.Optional; + +/** + * The state service is responsible for sending messages and processing received messages. It maintains state + * and tracks messages in and out and their successful acknowledgement (or not). + * + * The message handling layer will call into the state service with messages it has received, and the queue processor + * will use the state service to dispatch new outbound publish messages. + */ +public interface IMqttsnMessageStateService extends IMqttsnRegistry { + + /** + * Dispatch a new message to the transport layer, binding it to the state service en route for tracking. + * @param context - the recipient of the message + * @param message - the wire message to send + * @throws MqttsnException + */ + MqttsnWaitToken sendMessage(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException; + + /** + * Dispatch a new message to the transport layer, binding it to the state service en route for tracking. + * @param context + * @param queuedPublishMessage - reference to the queues message who originated this send (when its of type Publish); + * @throws MqttsnException + */ + MqttsnWaitToken sendMessage(IMqttsnContext context, TopicInfo info, QueuedPublishMessage queuedPublishMessage) throws MqttsnException; + + + /** + * Notify into the state service that a new message has arrived from the transport layer. + * @param context - the context from which the message was received + * @param message - the message received from the transport layer + * @return the messsage (if any) that was confirmed by the receipt of the inbound message + * @throws MqttsnException + */ + IMqttsnMessage notifyMessageReceived(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException; + + /** + * Join the message sent in waiting for the subsequent confirmation if it needs one + * @param context - The context to whom you are speaking + * @return An optional which will contain either the confirmation message associated with the + * message supplied OR optional NULL where the message does not require a reply + * @throws MqttsnExpectationFailedException - When no confirmation was recieved in the time period + * specified by the runtime options + */ + Optional waitForCompletion(IMqttsnContext context, MqttsnWaitToken token) throws MqttsnExpectationFailedException; + + /** + * Join the message sent in waiting for the subsequent confirmation if it needs one + * @param context - The context to whom you are speaking + * @param customWaitTime - a custom time to wait for the token to complete + * @return An optional which will contain either the confirmation message associated with the + * message supplied OR optional NULL where the message does not require a reply + * @throws MqttsnExpectationFailedException - When no confirmation was recieved in the time period + * specified by the runtime options + */ + Optional waitForCompletion(IMqttsnContext context, MqttsnWaitToken token, int customWaitTime) throws MqttsnExpectationFailedException; + + /** + * If a context has any messages inflight, notify the state service to clear them up, either requeuing or discarding + * according to configuration + * @param context - The context to whom you are speaking + * @throws MqttsnException - an error has occurred + */ + void clearInflight(IMqttsnContext context) throws MqttsnException ; + + /** + * Ccount the number of message inflight for a given direction and context + * @param context - The context to whom you are speaking + * @param direction - Inflight has 2 channels, inbound and outbound per the spec. Count in which direction. + * @return The number of messages inflight in a given direction + * @throws MqttsnException - an error has occurred + */ + int countInflight(IMqttsnContext context, InflightMessage.DIRECTION direction) throws MqttsnException ; + + /** + * Remove a specific message from inflight using its messageId, returning the message. + * If no message exists inflight with the corresponding ID, null will be returned. + * @param context - The context to whom you are speaking + * @param msgId - The message id to return + * @return - the corresponding message or NULL if not found + * @throws MqttsnException - an error has occurred + */ + InflightMessage removeInflight(IMqttsnContext context, int msgId) throws MqttsnException ; + + /** + * According to the state rules, are we in a position to send PUBLISH messages to the given context + * @param context - The context to whom you are speaking + * @return true if the state service think we're ok to send PUBLISHES to the device, else false + * @throws MqttsnException - an error has occurred + */ + boolean canSend(IMqttsnContext context) throws MqttsnException ; + + /** + * Mark a context active to have its outbound queue processed + * @param context - The context whose queue should be processed + * @throws MqttsnException - an error has occurred + */ + void scheduleFlush(IMqttsnContext context) throws MqttsnException ; + + /** + * UnMark a context active to have its outbound queue processed + * @param context - The context whose queue should be processed + * @throws MqttsnException - an error has occurred + */ + void unscheduleFlush(IMqttsnContext context) throws MqttsnException ; + + /** + * Tracks the point at which the last message was SENT to the context + * @param context - The context to which the message was sent + * @return Long representing the point at which the last message was SENT to the context + * @throws MqttsnException + */ + Long getMessageLastSentToContext(IMqttsnContext context) throws MqttsnException ; + + /** + * Tracks the point at which the last message was RECEIVED from the context + * @param context - The context to which the message was sent + * @return Long representing the point at which the last message was SENT to the context + * @throws MqttsnException + */ + Long getMessageLastReceivedFromContext(IMqttsnContext context) throws MqttsnException ; + + /** + * Tracks the point at which a message considered ACTIVE was last SENT or RECEIVIED from the context + * @param context - The context to which the message was sent + * @return Tracks the point at which a message considered ACTIVE was last SENT or RECEIVIED from the context + * @throws MqttsnException + */ +// Long getContextLastActive(IMqttsnContext context) throws MqttsnException ; +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnPermissionService.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnPermissionService.java new file mode 100644 index 00000000..f4d2b106 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnPermissionService.java @@ -0,0 +1,52 @@ +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.IMqttsnContext; + +/** + * Optional - when installed it will be consulted to determine whether a remote context can perform certain + * operations; + * + * CONNECT with the given clientId + * SUBSCRIBE to a given topicPath + * Granted Maximum Subscription Levels + * Eligibility to publish to a given path & size + */ +public interface IMqttsnPermissionService { + + /** + * Is a client allowed to CONNECT successfully. + * @param context - the context who would like to CONNECT + * @param clientId - the client Id they provided in the CONNECT dialog + * @return true if the client is allowed to CONNECT (yielding CONNACK ok) or false if not allowed (yielding CONNACK error) + * @throws MqttsnException an error occurred + */ + boolean allowConnect(IMqttsnContext context, String clientId) throws MqttsnException; + + /** + * Is a client allowed to SUBSCRIBE to a given topic path + * @param context - the context who would like to SUBSCRIBE + * @param topicPath - the topic path to subscribe to + * @return true if the client is allowed to SUBSCRIBE (yielding SUBACK ok) or false if not allowed (yielding SUBACK error) + * @throws MqttsnException an error occurred + */ + boolean allowedToSubscribe(IMqttsnContext context, String topicPath) throws MqttsnException; + + /** + * What is the maximum granted QoS allowed on a given topicPath. + * @param context - the context who would like to SUBSCRIBE + * @param topicPath - the topic path to subscribe to + * @return one of 0,1,2 matching the maximum QoS that can be granted for this subscription + * @throws MqttsnException an error occurred + */ + int allowedMaximumQoS(IMqttsnContext context, String topicPath) throws MqttsnException; + + /** + * Is the context allowed to publish to the given topicPath. + * @param context - the context who would like to PUBLISH + * @param topicPath - the topic path to publish to + * @param size - size of the data to be published + * @return true if the client is allowed to PUBLISH (yielding PUBLISH normal lifecycle) or false if not allowed (yielding PUBACK error) + * @throws MqttsnException an error occurred + */ + boolean allowedToPublish(IMqttsnContext context, String topicPath, int size, int QoS) throws MqttsnException; +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnPublishFailureListener.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnPublishFailureListener.java new file mode 100644 index 00000000..72770f57 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnPublishFailureListener.java @@ -0,0 +1,38 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.IMqttsnContext; + +import java.util.UUID; + +public interface IMqttsnPublishFailureListener { + + void sendFailure(IMqttsnContext context, UUID messageId, String topicName, int QoS, byte[] data, int retryCount); + +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnPublishReceivedListener.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnPublishReceivedListener.java new file mode 100644 index 00000000..157c5534 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnPublishReceivedListener.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.IMqttsnContext; + +public interface IMqttsnPublishReceivedListener { + + void receive(IMqttsnContext context, String topicName, int QoS, byte[] data); +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnPublishSentListener.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnPublishSentListener.java new file mode 100644 index 00000000..4ca5f604 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnPublishSentListener.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.IMqttsnContext; + +import java.util.UUID; + +public interface IMqttsnPublishSentListener { + + void sent(IMqttsnContext context, UUID messageId, String topicName, int QoS, byte[] data); +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnQueueProcessorStateService.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnQueueProcessorStateService.java new file mode 100644 index 00000000..c3088865 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnQueueProcessorStateService.java @@ -0,0 +1,28 @@ +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.IMqttsnContext; + +/** + * NOTE: this is optional + * When contributed, this controller is used by the queue processor to check if the application is in a fit state to + * offload messages to a remote (gateway or client), and is called back by the queue processor to be notified of + * the queue being empty after having been flushed. + */ +public interface IMqttsnQueueProcessorStateService { + + /** + * The application can determine if its in a position to send publish messages to the remote context + * @param context - The context who is to receive messages + * @return - Is the context in a position to receive message + * @throws MqttsnException - An error has occurred + */ + boolean canReceive(IMqttsnContext context) throws MqttsnException; + + /** + * Having flushed >= 1 messages to a context, this method will be notified to allow any final + * session related actions to be taken + * @return - Is the context in a position to receive message + * @throws MqttsnException - An error has occurred + */ + void queueEmpty(IMqttsnContext context) throws MqttsnException; +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnRegistry.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnRegistry.java new file mode 100644 index 00000000..7223b2f0 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnRegistry.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.IMqttsnContext; + +public interface IMqttsnRegistry extends IMqttsnService { + + void clear(IMqttsnContext context) throws MqttsnException; + + void clearAll() throws MqttsnException; + +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnRuntimeRegistry.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnRuntimeRegistry.java new file mode 100644 index 00000000..ec80bcfe --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnRuntimeRegistry.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.impl.AbstractMqttsnRuntime; +import org.slj.mqtt.sn.model.MqttsnOptions; + +import java.util.List; + +public interface IMqttsnRuntimeRegistry { + + void setRuntime(AbstractMqttsnRuntime runtime); + + void init(); + + MqttsnOptions getOptions(); + + AbstractMqttsnRuntime getRuntime(); + + IMqttsnTransport getTransport(); + + IMqttsnCodec getCodec(); + + IMqttsnMessageQueue getMessageQueue(); + + IMqttsnMessageFactory getMessageFactory(); + + IMqttsnMessageHandler getMessageHandler(); + + IMqttsnMessageStateService getMessageStateService(); + + INetworkAddressRegistry getNetworkRegistry(); + + IMqttsnTopicRegistry getTopicRegistry(); + + IMqttsnSubscriptionRegistry getSubscriptionRegistry(); + + IMqttsnContextFactory getContextFactory(); + + IMqttsnMessageQueueProcessor getQueueProcessor(); + + IMqttsnQueueProcessorStateService getQueueProcessorStateCheckService(); + + IMqttsnMessageRegistry getMessageRegistry(); + + IMqttsnPermissionService getPermissionService(); + + List getTrafficListeners(); +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnService.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnService.java new file mode 100644 index 00000000..1858e649 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnService.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +public interface IMqttsnService { + + void start(T runtime) throws MqttsnException; + + void stop() throws MqttsnException; + + boolean running(); +} \ No newline at end of file diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnSubscriptionRegistry.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnSubscriptionRegistry.java new file mode 100644 index 00000000..74232841 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnSubscriptionRegistry.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.Subscription; + +import java.util.List; +import java.util.Set; + +/** + * The subscription registry maintains a list of subscriptions against the remote context. On the gateway this + * is used to determine which clients are subscribed to which topics to enable outbound delivery. In client + * mode it tracks the subscriptions a client presently holds. + */ +public interface IMqttsnSubscriptionRegistry extends IMqttsnRegistry { + + /** + * Which QoS is a subscription held at + * @param context - the remote context who owns the subscription + * @param topicPath - the full clear text topicPath for the subscription e.g. foo/bar + * @return the QoS at which the subscription is held (0,1,2) + * @throws MqttsnException - an error occurred + */ + int getQos(IMqttsnContext context, String topicPath) throws MqttsnException; + + /** + * Create a new subscription for the context, or update the subscription if it already + * existed + * @param context - the remote context who owns the subscription + * @param topicPath - the full clear text topicPath for the subscription e.g. foo/bar + * @return the QoS at which the subscription is to be held (0,1,2) + * @return true if a NEW subscription or was created, false if one already existed (and was updated) + * @throws MqttsnException - an error occurred + */ + boolean subscribe(IMqttsnContext context, String topicPath, int QoS) throws MqttsnException; + + /** + * Remove and existing subscription for the context + * @param context - the remote context who owns the subscription + * @param topicPath - the full clear text topicPath for the subscription e.g. foo/bar + * @return true if a subscription was removed, false if one didnt exist + * @throws MqttsnException - an error occurred + */ + boolean unsubscribe(IMqttsnContext context, String topicPath) throws MqttsnException; + + /** + * This is called upon receipt of a message being received by a BROKER which necessitated expansion + * onto mutliple client queues. (Message expansion). + * + * @param topicPath - the full clear text topicPath for the subscription e.g. foo/bar + * @return a list of context which hold valid subscriptions for the supplied topic (including wildcard matching) + * @throws MqttsnException + */ + List matches(String topicPath) throws MqttsnException ; + + + /** + * A set of all the tracked subscriptions for the context + * + * @param context - the remote context who owns the subscriptions + * @return a set of subscriptions to which the context is subscribed + * @throws MqttsnException + */ + Set readSubscriptions(IMqttsnContext context) throws MqttsnException ; +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnTopicRegistry.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnTopicRegistry.java new file mode 100644 index 00000000..255fc0ba --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnTopicRegistry.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.TopicInfo; + +/** + * The topic registry is responsible for tracking, storing and determining the correct alias + * to use for a given remote context and topic combination. The topic registry will be cleared + * according to session lifecycle rules. + */ +public interface IMqttsnTopicRegistry extends IMqttsnRegistry { + + TopicInfo normalize(byte topicIdType, byte[] topicData, boolean normalAsLong) throws MqttsnException; + + TopicInfo register(IMqttsnContext context, String topicPath) throws MqttsnException; + + void register(IMqttsnContext context, String topicPath, int alias) throws MqttsnException; + + boolean registered(IMqttsnContext context, String topicPath) throws MqttsnException; + + TopicInfo lookup(IMqttsnContext context, String topicPath) throws MqttsnException; + + TopicInfo lookup(IMqttsnContext context, String topicPath, boolean confirmedOnly) throws MqttsnException; + + String topicPath(IMqttsnContext context, TopicInfo topicInfo, boolean considerContext) throws MqttsnException ; + + //-- lookup specific parts of the registry + Integer lookupRegistered(IMqttsnContext context, String topicPath, boolean confirmedOnly) throws MqttsnException; + + Integer lookupRegistered(IMqttsnContext context, String topicPath) throws MqttsnException; + + String lookupRegistered(IMqttsnContext context, int topicAlias) throws MqttsnException; + + Integer lookupPredefined(IMqttsnContext context, String topicPath) throws MqttsnException; + + String lookupPredefined(IMqttsnContext context, int topicAlias) throws MqttsnException; + + void clear(IMqttsnContext context, boolean hardClear) throws MqttsnException; + +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnTrafficListener.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnTrafficListener.java new file mode 100644 index 00000000..bb7abf14 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnTrafficListener.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.INetworkContext; + +/** + * Traffic listeners can contributed to the runtime to be notified of any traffic processed by + * the transport layer. Listeners are not able to affect the traffic in transit or the business + * logic executed during the course of the traffic, they are merely observers. + * + * The listeners ~may be notified asynchronously from the application, and thus they cannot be relied + * upon to give an absolute timeline of traffic. + * + */ +public interface IMqttsnTrafficListener { + + /** + * Traffic has been successfully sent by the transport layer to the context + * @param context - the context from which the transport originated + * @param data - the raw data + * @param message - the data that was sent/received + */ + void trafficSent(INetworkContext context, byte[] data, IMqttsnMessage message); + + /** + * Traffic has been received by the transport layer + * @param context - the context from which the transport originated + * @param data - the raw data + * @param message - the data that was sent/received + */ + void trafficReceived(INetworkContext context, byte[] data, IMqttsnMessage message); + +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnTransport.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnTransport.java new file mode 100644 index 00000000..8cb0f28d --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/IMqttsnTransport.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.INetworkContext; + +import java.nio.ByteBuffer; + +/** + * The transport layer is responsible for managing the receiving and sending of messages over some connection. + * No session is assumed by the application and the connection is considered stateless at this point. + * It is envisaged implementations will include UDP (with and without DTLS), TCP-IP (with and without TLS), + * BLE and ZigBee. + * + * Please refer to {@link org.slj.mqtt.sn.impl.AbstractMqttsnTransport} and sub-class your own implementations + * or choose an existing implementation out of the box. + * + * @see {@link org.slj.mqtt.sn.net.MqttsnUdpTransport} for an example of an out of the box implementation. + */ +public interface IMqttsnTransport { + + void receiveFromTransport(INetworkContext context, ByteBuffer buffer); + + void writeToTransport(INetworkContext context, IMqttsnMessage message) throws MqttsnException ; + + void broadcast(IMqttsnMessage message) throws MqttsnException ; + + void connectionLost(INetworkContext context, Throwable t); + + boolean restartOnLoss(); + +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/INetworkAddressRegistry.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/INetworkAddressRegistry.java new file mode 100644 index 00000000..e6f8b55a --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/INetworkAddressRegistry.java @@ -0,0 +1,43 @@ +package org.slj.mqtt.sn.spi; + +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.INetworkContext; +import org.slj.mqtt.sn.net.NetworkAddress; + +import java.net.InetAddress; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +/** + * The network registry maintains a list of known network contexts against a remote address ({@link NetworkAddress}). + * It exposes functionality to wait for discovered contexts as well as returning a list of valid broadcast addresses. + */ +public interface INetworkAddressRegistry { + + INetworkContext getContext(NetworkAddress address) throws NetworkRegistryException ; + + INetworkContext getContext(IMqttsnContext sessionContext); + + IMqttsnContext getSessionContext(INetworkContext networkContext); + + Optional first() throws NetworkRegistryException ; + + void putContext(INetworkContext context) throws NetworkRegistryException ; + + void bindContexts(INetworkContext context, IMqttsnContext sessionContext); + + Optional waitForContext(int time, TimeUnit unit) throws InterruptedException, NetworkRegistryException; + + List getAllBroadcastAddresses() throws NetworkRegistryException ; + + Iterator iterator() throws NetworkRegistryException ; + + boolean removeExistingClientId(String clientId); + + boolean hasBoundSessionContext(INetworkContext context); + + Optional findForClientId(String clientId); +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/MqttsnException.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/MqttsnException.java new file mode 100644 index 00000000..f63e5526 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/MqttsnException.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +public class MqttsnException extends Exception { + + public MqttsnException() { + } + + public MqttsnException(String message) { + super(message); + } + + public MqttsnException(String message, Throwable cause) { + super(message, cause); + } + + public MqttsnException(Throwable cause) { + super(cause); + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/MqttsnExpectationFailedException.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/MqttsnExpectationFailedException.java new file mode 100644 index 00000000..62f83b65 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/MqttsnExpectationFailedException.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +public class MqttsnExpectationFailedException extends MqttsnException { + + public MqttsnExpectationFailedException() { + } + + public MqttsnExpectationFailedException(String message) { + super(message); + } + + public MqttsnExpectationFailedException(String message, Throwable cause) { + super(message, cause); + } + + public MqttsnExpectationFailedException(Throwable cause) { + super(cause); + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/MqttsnRuntimeException.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/MqttsnRuntimeException.java new file mode 100644 index 00000000..a4b06b88 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/MqttsnRuntimeException.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +public class MqttsnRuntimeException extends RuntimeException { + + public MqttsnRuntimeException() { + } + + public MqttsnRuntimeException(String message) { + super(message); + } + + public MqttsnRuntimeException(String message, Throwable cause) { + super(message, cause); + } + + public MqttsnRuntimeException(Throwable cause) { + super(cause); + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/MqttsnSecurityException.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/MqttsnSecurityException.java new file mode 100644 index 00000000..753596a4 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/MqttsnSecurityException.java @@ -0,0 +1,19 @@ +package org.slj.mqtt.sn.spi; + +public class MqttsnSecurityException extends SecurityException { + + public MqttsnSecurityException() { + } + + public MqttsnSecurityException(String s) { + super(s); + } + + public MqttsnSecurityException(String message, Throwable cause) { + super(message, cause); + } + + public MqttsnSecurityException(Throwable cause) { + super(cause); + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/MqttsnService.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/MqttsnService.java new file mode 100644 index 00000000..231a3ca8 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/MqttsnService.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.spi; + +import java.util.logging.Logger; + +public abstract class MqttsnService implements IMqttsnService { + + protected final Logger logger = Logger.getLogger(getClass().getName()); + protected T registry; + protected volatile boolean running = false; + + public MqttsnService(){ + } + + public void start(T runtime) throws MqttsnException { + this.registry = runtime; + running = true; + } + + public void stop() throws MqttsnException { + running = false; + } + + protected T getRegistry(){ + return registry; + } + + public boolean running(){ + return running; + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/NetworkRegistryException.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/NetworkRegistryException.java new file mode 100644 index 00000000..fb22d5f4 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/spi/NetworkRegistryException.java @@ -0,0 +1,19 @@ +package org.slj.mqtt.sn.spi; + +public class NetworkRegistryException extends Exception { + + public NetworkRegistryException() { + } + + public NetworkRegistryException(String message) { + super(message); + } + + public NetworkRegistryException(String message, Throwable cause) { + super(message, cause); + } + + public NetworkRegistryException(Throwable cause) { + super(cause); + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/utils/MqttsnUtils.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/utils/MqttsnUtils.java new file mode 100644 index 00000000..181dcec4 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/utils/MqttsnUtils.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.utils; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.model.MqttsnClientState; +import org.slj.mqtt.sn.model.MqttsnWaitToken; +import org.slj.mqtt.sn.spi.IMqttsnMessage; +import org.slj.mqtt.sn.spi.MqttsnException; +import org.slj.mqtt.sn.spi.MqttsnExpectationFailedException; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class MqttsnUtils { + + private static Logger logger = Logger.getLogger(MqttsnUtils.class.getName()); + + public static double percentOf(double val, double of){ + return val / of * 100; + } + + public static byte[] arrayOf(int size, byte fill){ + byte[] a = new byte[size]; + Arrays.fill(a, fill); + return a; + } + + public static boolean in(MqttsnClientState state, MqttsnClientState... options){ + if(options == null) return false; + for (int i = 0; i < options.length; i++) { + if(options[i] == state) return true; + } + return false; + } + + public static void responseCheck(MqttsnWaitToken token, Optional response) + throws MqttsnExpectationFailedException{ + if(response.isPresent() && + response.get().isErrorMessage()){ + logger.log(Level.WARNING, "error response received from gateway, operation failed; throw to application"); + throw new MqttsnExpectationFailedException("error response received from gateway, operation failed"); + } + if(token.isError()){ + logger.log(Level.WARNING, "token was marked invalid by state machine; throw to application"); + throw new MqttsnExpectationFailedException("token was marked invalid by state machine"); + } + } + + public static int getNextLeaseId(Collection used, int startAt) throws MqttsnException { + if(used.isEmpty()) return startAt; + if(used.size() == ((0xFFFF - startAt) + 1)) throw new MqttsnException("all leases taken"); + TreeSet sortedIds = new TreeSet<>(used); + Integer highest = sortedIds.last(); + if(highest >= 0xFFFF) + throw new MqttsnException("no alias left for use for client"); + + int nextValue = highest.intValue(); + do { + nextValue++; + if(!used.contains(nextValue)) return nextValue; + } while(nextValue <= 0xFFFF); + throw new MqttsnException("unable to assigned lease client"); + } + + public static String getDurationString(long millis) { + + if(millis < 0) { + throw new IllegalArgumentException("must be greater than zero!"); + } + + if(millis < 1000){ + return String.format("%s millisecond%s", millis, millis > 1 ? "s" : ""); + } + + long days = TimeUnit.MILLISECONDS.toDays(millis); + millis -= TimeUnit.DAYS.toMillis(days); + long hours = TimeUnit.MILLISECONDS.toHours(millis); + millis -= TimeUnit.HOURS.toMillis(hours); + long minutes = TimeUnit.MILLISECONDS.toMinutes(millis); + millis -= TimeUnit.MINUTES.toMillis(minutes); + long seconds = TimeUnit.MILLISECONDS.toSeconds(millis); + + StringBuilder sb = new StringBuilder(); + + if(days > 0) { + sb.append(days); + sb.append(String.format(" day%s, ", days > 1 ? "s" : "")); + } + + if(days > 0 || hours > 0) { + sb.append(hours); + sb.append(String.format(" hour%s, ", hours > 1 || hours == 0 ? "s" : "")); + } + + if(hours > 0 || days > 0 || minutes > 0) { + sb.append(minutes); + sb.append(String.format(" minute%s, ", minutes > 1 || minutes == 0 ? "s" : "")); + } + + sb.append(seconds); + sb.append(String.format(" second%s", seconds > 1 ? "s" : "")); + + return(sb.toString()); + } + + public static boolean contains(T[] haystack, T needle){ + if(haystack == null || haystack.length == 0) return false; + for (int i = 0; i < haystack.length; i++) { + if(Objects.equals(haystack[i], needle)){ + return true; + } + } + return false; + } + + public static boolean validTopicScheme(int topicIdType, byte[] topicBytes, boolean topicDataAsString) { + if(topicIdType == MqttsnConstants.TOPIC_PREDEFINED){ + return topicBytes.length == 2; + } else if(topicIdType == MqttsnConstants.TOPIC_NORMAL){ + return topicDataAsString ? validTopicName(new String(topicBytes, MqttsnConstants.CHARSET)) : topicBytes.length == 2; + } else if(topicIdType == MqttsnConstants.TOPIC_SHORT){ + return topicBytes.length == 2; + } + else + return false; + } + + public static boolean validTopicName(String topicString) { + return topicString != null && topicString.trim().length() > 0; + } + + public static boolean validQos(int value) { + if(value < -1 || value > 2) + return false; + return true; + } + + public static boolean validUInt16(int value) { + if(value < 0 || value > MqttsnConstants.USIGNED_MAX_16) + return false; + return true; + } + + public static boolean validInt8(int value) throws MqttsnExpectationFailedException{ + if(value < 0 || value > MqttsnConstants.USIGNED_MAX_8) + return false; + return true; + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/utils/StringTable.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/utils/StringTable.java new file mode 100644 index 00000000..1dbff8bc --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/utils/StringTable.java @@ -0,0 +1,263 @@ +package org.slj.mqtt.sn.utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + *

StringTable

+ *

+ * StringTable allows developers to quickly visualise tabulated data in user interfaces or logs + * by providing simple API to construct and output the data. + * + * Please see {@link org.codesaurus.utils.StringTableWriters} for available output formats + *

+ * + *
+ *      StringTable st = new StringTable("First Column", "Second Column", "Third Column");
+ *      for (int i = 0; i < 10; i++) {
+ *          st.addRow(i, i, i);
+ *      }
+ *      System.out.println(StringTableWriters.writeStringTableAsCSV(st));
+ *      System.out.println(StringTableWriters.writeStringTableAsASCII(st));
+ *      System.out.println(StringTableWriters.writeStringTableAsHtml(st, false, 2));
+ * 
+ * + * @author Simon Johnson + * @since 01/2004 - initial implementation + * + * 2017 - rollup and row index support + * 2019 - table sort added + * matt h - fixed double numerical sort + */ +public class StringTable { + + private boolean rowIndexAdded = false; + private String tableName; + private List headings; + private List footer; + private List rows = new ArrayList(); + private Map> COLUMN_SORTERS = new HashMap<>(); + + private static final Comparator DEFAULT_SORT = new Comparator() { + public int compare(String o1, String o2) { + String val1 = o1.trim(); + String val2 = o2.trim(); + try { + double d1 = Double.parseDouble(val1); + double d2 = Double.parseDouble(val2); + return Double.compare(d1, d2); + } catch (Exception e) { + } + return String.CASE_INSENSITIVE_ORDER.compare(o1, o2); + } + }; + + public StringTable() { + } + + /** + * Construct a StringTable with defined table headings + * @param headings + */ + public StringTable(String...headings){ + this(null, headings); + } + + public StringTable(String tableName, String[] headings){ + + this.tableName = tableName; + this.headings = new ArrayList(headings.length); + this.headings.addAll(Arrays.asList(headings)); + } + + public List getHeadings() { + return headings; + } + + public void setHeadings(List headings) { + this.headings = headings; + } + + public void setTableName(String tableName){ + this.tableName = tableName; + } + + public String getTableName(){ + return this.tableName; + } + + public void setRows(List rows){ + this.rows = rows; + } + + public List getRows(){ + return rows; + } + + public List getFooter(){ + return footer; + } + + /** + * Show a row index column being output during rendering phase + * @default hidden + */ + public void showRowIndex(){ + rowIndexAdded = true; + } + + /** + * Hide the row index column + */ + public void hideRowIndex(){ + rowIndexAdded = false; + } + + /** + * Apply natural sort on the column index specified. + * Where the data contains only numeric data it will use natural numeric sort + * @param colIdx - Zero indexed value to determine which column to sort by + * @param reverse - Should the sort direct be natural order reversed + */ + public synchronized void sort(final int colIdx, boolean reverse) { + Comparator wrapper = new Comparator() { + public int compare(String[] o1, String[] o2) { + if(o1.length >= (colIdx + 1) && o2.length >= (colIdx + 1)){ + return getComparatorForColumn(colIdx).compare(o1[colIdx].trim(), o2[colIdx].trim()); + } + return 0; + } + }; + rows.sort(reverse ? wrapper.reversed() : wrapper); + } + + /** + * Set the comparator for a given column index allowing full control + * of data sorting + * @param colIdx - Zero indexed value to determine which column to assign comparator to + * @param c - your custom comparator + */ + public void addColumnComparator(int colIdx, Comparator c){ + COLUMN_SORTERS.put(colIdx, c); + } + + private Comparator getComparatorForColumn(int colIdx){ + Comparator c = COLUMN_SORTERS.get(colIdx); + if(c == null) { + c = DEFAULT_SORT; + } + return c; + } + + /** + * Add data to the rollup (footer) of the table + */ + public synchronized void addFooter(String... row){ + if(row == null || row.length == 0) return; + this.footer = new ArrayList(row.length); + this.footer.addAll(Arrays.asList(row)); + } + + public synchronized void addRow(Object... row){ + + if(row == null || row.length == 0) return; + String[] a = new String[row.length]; + for (int i = 0; i < row.length; i++) { + a[i] = String.valueOf(row[i]); + } + rows.add(a); + } + + public void addRows(List rows){ + + Iterator itr = rows.iterator(); + while(itr.hasNext()){ + String[] row = itr.next(); + addRow((Object[])row); + } + } + + /** + * Merge data from other tables into this table + */ + public void combine(StringTable... tables){ + if(tables != null){ + for (StringTable stringTable : tables) { + if(StringTable.getColumnCount(stringTable) == + StringTable.getColumnCount(this)){ + addRows(stringTable.getRows()); + } + } + } + } + + public synchronized void addCellValue(Object s){ + String[] arr = rows.size() == 0 ? null : rows.get(rows.size() - 1); + if(arr == null || arr.length >= getHeadings().size()){ + arr = nextRow(); + } + String[] newarr = new String[arr.length+1]; + System.arraycopy(arr, 0, newarr, 0, arr.length); + newarr[arr.length] = s == null ? "" : String.valueOf(s); + rows.set(rows.size() - 1, newarr); + } + + private String[] nextRow(){ + String[] arr = new String[0]; + rows.add(arr); + return arr; + } + + /** + * Add a row separator into your table data + */ + public void addSeparator(){ + rows.add(null); + } + + public static String getColumnName(StringTable table, int idx){ + if(table.rowIndexAdded) { + if(idx == 0) return "Row"; + idx = idx - 1; + } + return table.getHeadings().get(idx); + } + + /** + * returns the number of columns in the given table + */ + public static int getColumnCount(StringTable table){ + int count = table.getHeadings().size(); + if(table.rowIndexAdded) count++; + return count; + } + + /** + * returns the number of rows in the given table + */ + public static int size(StringTable table){ + return table.getRows().size(); + } + + public String getCellValue(int rowIdx, int colIdx){ + if(rowIndexAdded) { + if(colIdx == 0) return "" + (rowIdx + 1); + colIdx = colIdx - 1; + } + String val = rows.get(rowIdx)[colIdx]; + return val == null ? "" : val; + } + + public void addRows(Collection collection, Function mapper) { + if (null != collection && null != mapper) { + collection.stream().forEach(r -> addRow(mapper.apply(r))); + } + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/utils/StringTableWriters.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/utils/StringTableWriters.java new file mode 100644 index 00000000..5b0eaea2 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/utils/StringTableWriters.java @@ -0,0 +1,272 @@ +package org.slj.mqtt.sn.utils; + +import java.util.Iterator; +import java.util.List; + +/** + * Utilities to convert tabulated model into various output formats + * + * @since 01/2004 + * @author Simon Johnson + * + * Contributors; + * 2019 Matt h - improved ASCII output + */ +public class StringTableWriters { + + static final char PLUS = '+'; + static final char HYPHEN = '-'; + static final char UNDERSCORE = '_'; + static final char PIPE = '|'; + static final char WHITESPACE = ' '; + static final String NEWLINE = System.lineSeparator(); + + public static String writeStringTableAsCSV(StringTable st){ + CSVTextWriter textTable = new CSVTextWriter(); + return textTable.write(st); + } + + public static String writeStringTableAsASCII(StringTable st){ + ASCIITableWriter writer = new ASCIITableWriter(); + return writer.write(st); + } + + public static String writeStringTableAsHTML(StringTable st, boolean borders, int padding){ + HTMLTextWriter htmlTable = new HTMLTextWriter(borders, padding); + return htmlTable.write(st); + } + + private static class HTMLTextWriter { + + boolean borders = false; + int padding; + + public HTMLTextWriter(boolean borders, int padding) { + this.borders = borders; + this.padding = padding; + } + + public String write(StringTable table) { + + StringBuilder sb = new StringBuilder(); + + sb.append(""); + sb.append(""); + sb.append(""); + + //-- headings + List headings = table.getHeadings(); + Iterator itr = headings.iterator(); + while(itr.hasNext()){ + String heading = itr.next(); + sb.append(""); + } + + sb.append(""); + sb.append(""); + + //-- data + List data = table.getRows(); + if(data != null && !data.isEmpty()) { + + Iterator dataItr = data.iterator(); + while(dataItr.hasNext()){ + sb.append(""); + String[] row = dataItr.next(); + for (int i = 0; i < row.length; i++) { + sb.append(""); + } + + sb.append(""); + } + } + + //-- rollup + List footers = table.getFooter(); + if(footers != null && !footers.isEmpty()) { + Iterator footerItr = footers.iterator(); + sb.append(""); + while(footerItr.hasNext()){ + sb.append(""); + } + sb.append(""); + } + + sb.append("
"); + sb.append(heading.trim()); + sb.append("
"); + sb.append(row[i].trim()); + sb.append("
"); + String footer = footerItr.next(); + sb.append(footer.trim()); + sb.append("
"); + return sb.toString(); + } + } + + private static class CSVTextWriter { + + static final char SEP = ','; + static final String NEWLINE = System.lineSeparator(); + + public String write(StringTable table) { + return write(table, SEP); + } + + public String write(StringTable table, char sep) { + + StringBuilder sb = new StringBuilder(); + + //-- headings + List headings = table.getHeadings(); + Iterator itr = headings.iterator(); + while(itr.hasNext()){ + String heading = itr.next(); + sb.append(heading.trim()); + if(itr.hasNext()) sb.append(sep); + } + + //-- data + List data = table.getRows(); + if(data != null && !data.isEmpty()) { + sb.append(NEWLINE); + + Iterator dataItr = data.iterator(); + while(dataItr.hasNext()){ + String[] row = dataItr.next(); + for (int i = 0; i < row.length; i++) { + if(i > 0) sb.append(sep); + sb.append(row[i].trim()); + } + + if(dataItr.hasNext()) sb.append(NEWLINE); + } + } + + //-- rollup + List footers = table.getFooter(); + if(footers != null && !footers.isEmpty()) { + sb.append(NEWLINE); + + Iterator footerItr = footers.iterator(); + while(footerItr.hasNext()){ + String footer = footerItr.next(); + sb.append(footer.trim()); + if(footerItr.hasNext()) sb.append(sep); + } + } + + return sb.toString(); + } + } + + private static class ASCIITableWriter { + + private StringTable table; + private int rowCount; + private int columnCount; + private int[] columnWidths; + private StringBuilder text; + + private String write(StringTable table) { + // Initialise + this.table = table; + this.rowCount = table.getRows().size(); + this.columnCount = StringTable.getColumnCount(table); + + // Calculate column positions. + columnWidths = getColumnWidths(); + + // Write table. + return writeTable(); + } + + private int[] getColumnWidths() { + int[] columnWidths = new int[columnCount]; + for (int i = 0; i < columnCount; i++) { + columnWidths[i] = getColumnWidth(i); + } + return columnWidths; + } + + private int getColumnWidth(int columnIndex) { + int columnWidth = StringTable.getColumnName(table, columnIndex).length(); + for (int i = 0; i < rowCount; i++) { + int valueWidth = table.getCellValue(i, columnIndex).length(); + columnWidth = Math.max(columnWidth, valueWidth); + } + if (null != table.getFooter() && table.getFooter().size() == columnCount) { + String footer = table.getFooter().get(columnIndex); + if (null != footer && footer.length() > columnWidth) { + columnWidth = footer.length(); + } + } + return columnWidth; + } + + private String writeTable() { + text = new StringBuilder(); + + if (null != table.getTableName()) { + text.append(table.getTableName().toUpperCase()); + writeLine(); + } + + writeSeparatorRow(); + writeColumnNames(); + writeSeparatorRow(); + for (int i = 0; i < rowCount; i++) { + writeRow(i); + } + writeSeparatorRow(); + + if (null != table.getFooter() && table.getFooter().size() == columnCount) { + for (int i = 0; i < columnCount; i++) { + writeValue(table.getFooter().get(i), i); + } + writeLine(); + writeSeparatorRow(); + } + + return text.toString(); + } + + private void writeSeparatorRow() { + StringBuilder sb = new StringBuilder(); + for (int width : columnWidths) { + for (int i = 0; i < width + 4; i++) { + sb.append((i == 0 || i == width + 3) ? PLUS : HYPHEN); + } + } + text.append(sb); + writeLine(); + } + + private void writeColumnNames() { + for (int i = 0; i < columnCount; i++) { + writeValue(StringTable.getColumnName(table, i), i); + } + writeLine(); + } + + private void writeRow(int rowIndex) { + for (int i = 0; i < columnCount; i++) { + writeValue(table.getCellValue(rowIndex, i), i); + } + writeLine(); + } + + private void writeValue(String value, int columnIndex) { + StringBuilder sb = new StringBuilder("| ").append(value); + while (sb.length() < columnWidths[columnIndex] + 2) { + sb.append(WHITESPACE); + } + sb.append(" |"); + text.append(sb); + } + + private void writeLine() { + text.append(NEWLINE); + } + } +} diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/utils/TopicPath.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/utils/TopicPath.java new file mode 100644 index 00000000..505a47bb --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/utils/TopicPath.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.utils; + +import java.io.Serializable; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class TopicPath { + +// static final String TOPIC_REGX = "^((^\\/?|\\/)([A-Za-z0-9\\.\\-\\$\\:_]+|$)?)*$"; + static final String SUBCRIPTION_REGX = "^((^\\/?|\\/)([A-Za-z0-9\\.\\-\\$\\:_]+|\\+|#$)?)*$"; + static final String WILDCARD = "#"; + static final String WILDSEG = "+"; + static final String PATHSEP = "/"; + + private Topic topic; + + public TopicPath(String topicPath){ + this.topic = new Topic(topicPath); + } + + public boolean matches(String topicPath) throws ParseException { + Topic matchTopic = new Topic(topicPath); + List msgTokens = topic.getTokens(); + List subscriptionTokens = matchTopic.getTokens(); + int i = 0; + for (; i < subscriptionTokens.size(); i++) { + Token subToken = subscriptionTokens.get(i); + if (subToken != Token.MULTI && subToken != Token.SINGLE) { + if (i >= msgTokens.size()) { + return false; + } + Token msgToken = msgTokens.get(i); + if (!msgToken.equals(subToken)) { + return false; + } + } else { + if (subToken == Token.MULTI) { + return true; + } + if (subToken == Token.SINGLE) { + } + } + } + return i == msgTokens.size(); + } + + public static boolean isWild(String topicPath){ + return topicPath != null && topicPath.contains(WILDCARD) ||topicPath.contains(WILDSEG) ; + } + + public static boolean isValidTopic(String topicPath, int maxLength){ + return topicPath != null && topicPath.trim().length() > 0 && topicPath.trim().length() < maxLength; +// TOPIC_REGX.matches(topicPath); + } + + public static boolean isValidSubscription(String topicPath, int maxLength){ + return topicPath != null && topicPath.trim().length() > 0 && topicPath.trim().length() < maxLength; +// && SUBCRIPTION_REGX.matches(topicPath); + } + + public String toString(){ + return topic.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TopicPath topicPath = (TopicPath) o; + return Objects.equals(topic, topicPath.topic); + } + + @Override + public int hashCode() { + return Objects.hash(topic); + } + + private static class Token { + + static final Token EMPTY = new Token(""); + static final Token MULTI = new Token(WILDCARD); + static final Token SINGLE = new Token(WILDSEG); + final String name; + + protected Token(String s) { + name = s; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 29 * hash + (this.name != null ? this.name.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Token other = (Token) obj; + if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) { + return false; + } + return true; + } + + @Override + public String toString() { + return name; + } + } + + private static class Topic implements Serializable { + + private final String topicPath; + private transient List tokens; + + public Topic(String topic) { + this.topicPath = topic; + } + + public List getTokens() throws ParseException { + if (tokens == null) { + tokens = parseTopic(topicPath); + } + return tokens; + } + + private List parseTopic(String topic) throws ParseException { + List res = new ArrayList<>(); + String[] arr = topic.split(PATHSEP); + if (arr.length == 0) { + res.add(Token.EMPTY); + } + if (topic.endsWith(PATHSEP)) { + String[] newArr = new String[arr.length + 1]; + System.arraycopy(arr, 0, newArr, 0, arr.length); + newArr[arr.length] = ""; + arr = newArr; + } + for (int i = 0; i < arr.length; i++) { + String s = arr[i]; + if (s.isEmpty()) { + res.add(Token.EMPTY); + } else if (s.equals(WILDCARD)) { + if (i != arr.length - 1) { + throw new ParseException("bad topic format - the multi symbol (#) has to be the last one after a separator", i); + } + res.add(Token.MULTI); + } else if (s.contains(WILDCARD)) { + throw new ParseException("bad topic format - invalid subtopic name: " + s, i); + } else if (s.equals(WILDSEG)) { + res.add(Token.SINGLE); + } else if (s.contains(WILDSEG)) { + throw new ParseException("bad topic format - invalid subtopic name: " + s, i); + } else { + res.add(new Token(s)); + } + } + return res; + } + + @Override + public String toString() { + return topicPath; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Topic other = (Topic) obj; + return Objects.equals(this.topicPath, other.topicPath); + } + + @Override + public int hashCode() { + return topicPath.hashCode(); + } + } + + public static final void main(String[] args){ + } +} \ No newline at end of file diff --git a/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/utils/TransientObjectLocks.java b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/utils/TransientObjectLocks.java new file mode 100644 index 00000000..f7222650 --- /dev/null +++ b/mqtt-sn-core/src/main/java/org/slj/mqtt/sn/utils/TransientObjectLocks.java @@ -0,0 +1,116 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.utils; + +import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.WeakHashMap; + +public class TransientObjectLocks { + + private final Map> lockMap + = new WeakHashMap<>(); + + public IMutex mutex(String id) { + + if (id == null) { + throw new NullPointerException(); + } + + IMutex key = new IdMutex(id); + + synchronized (lockMap) { + + WeakReference ref = lockMap.get(key); + if (ref == null) { + + lockMap.put(key, + new WeakReference(key)); + return key; + } + + IMutex mutex = (IMutex) ref.get(); + if (mutex == null) { + + lockMap.put(key, + new WeakReference(key)); + return key; + } + return mutex; + } + } + + public int getMutexCount() { + + synchronized (lockMap) { + + return lockMap.size(); + } + } + + static interface IMutex { } + + static class IdMutex implements IMutex { + + private final String id; + + protected IdMutex(String id) { + + this.id = id; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + IdMutex other = (IdMutex) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + + public String toString() { + return id; + } + } +} diff --git a/mqtt-sn-gateway-paho-connector/README.md b/mqtt-sn-gateway-paho-connector/README.md new file mode 100644 index 00000000..d261934c --- /dev/null +++ b/mqtt-sn-gateway-paho-connector/README.md @@ -0,0 +1,2 @@ +# MQTT-SN Paho Connector +Provides the backend TCP/IP connection to the MQTT broker. \ No newline at end of file diff --git a/mqtt-sn-gateway-paho-connector/dependency-reduced-pom.xml b/mqtt-sn-gateway-paho-connector/dependency-reduced-pom.xml new file mode 100644 index 00000000..6a5c728b --- /dev/null +++ b/mqtt-sn-gateway-paho-connector/dependency-reduced-pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + org.slj + mqtt-sn-gateway-paho-connector + 1.0.0 + + mqtt-sn-gateway-${version} + + + maven-shade-plugin + 2.3 + + + package + + shade + + + + + org.slj.mqtt.sn.gateway.impl.AggregatingGatewayInteractiveMain + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + + junit + junit + 4.13 + test + + + hamcrest-core + org.hamcrest + + + + + + 1.5.0 + 3.8.1 + 2.4.3 + 1.8 + UTF-8 + 1.8 + 4.13 + + + diff --git a/mqtt-sn-gateway-paho-connector/mqtt-sn-gateway-paho-connector.iml b/mqtt-sn-gateway-paho-connector/mqtt-sn-gateway-paho-connector.iml new file mode 100644 index 00000000..43579f4a --- /dev/null +++ b/mqtt-sn-gateway-paho-connector/mqtt-sn-gateway-paho-connector.iml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mqtt-sn-gateway-paho-connector/pom.xml b/mqtt-sn-gateway-paho-connector/pom.xml new file mode 100644 index 00000000..362b0619 --- /dev/null +++ b/mqtt-sn-gateway-paho-connector/pom.xml @@ -0,0 +1,120 @@ + + + + + 4.0.0 + + org.slj + mqtt-sn-gateway-paho-connector + 1.0.0 + + + UTF-8 + 4.13 + 3.8.1 + 1.8 + 1.8 + 2.4.3 + 1.5.0 + + + + + org.slj + mqtt-sn-core + 1.0.0 + + + org.slj + mqtt-sn-codec + 1.0.0 + + + org.slj + mqtt-sn-gateway + 1.0.0 + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.5 + + + junit + junit + ${junit.version} + test + + + + + mqtt-sn-gateway-${version} + + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + + + package + + shade + + + + + + + org.slj.mqtt.sn.gateway.impl.AggregatingGatewayInteractiveMain + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + \ No newline at end of file diff --git a/mqtt-sn-gateway-paho-connector/src/main/java/org/slj/mqtt/sn/gateway/impl/AggregatingGatewayInteractiveMain.java b/mqtt-sn-gateway-paho-connector/src/main/java/org/slj/mqtt/sn/gateway/impl/AggregatingGatewayInteractiveMain.java new file mode 100644 index 00000000..20ac9689 --- /dev/null +++ b/mqtt-sn-gateway-paho-connector/src/main/java/org/slj/mqtt/sn/gateway/impl/AggregatingGatewayInteractiveMain.java @@ -0,0 +1,58 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.gateway.impl; + +import org.slj.mqtt.sn.codec.MqttsnCodecs; +import org.slj.mqtt.sn.gateway.cli.MqttsnInteractiveGateway; +import org.slj.mqtt.sn.gateway.cli.MqttsnInteractiveGatewayLauncher; +import org.slj.mqtt.sn.gateway.impl.broker.MqttsnAggregatingBrokerService; +import org.slj.mqtt.sn.gateway.spi.broker.MqttsnBrokerOptions; +import org.slj.mqtt.sn.impl.AbstractMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.model.MqttsnOptions; +import org.slj.mqtt.sn.spi.IMqttsnTransport; + +public class AggregatingGatewayInteractiveMain { + public static void main(String[] args) throws Exception { + MqttsnInteractiveGatewayLauncher.launch(new MqttsnInteractiveGateway() { + protected AbstractMqttsnRuntimeRegistry createRuntimeRegistry(MqttsnOptions options, IMqttsnTransport transport) { + + MqttsnBrokerOptions brokerOptions = new MqttsnBrokerOptions(). + withHost(hostName). + withPort(port). + withUsername(username). + withPassword(password); + + return MqttsnGatewayRuntimeRegistry.defaultConfiguration(options). + withBrokerConnectionFactory(new PahoMqttsnBrokerConnectionFactory()). + withBrokerService(new MqttsnAggregatingBrokerService(brokerOptions)). + withTransport(createTransport()). + withCodec(MqttsnCodecs.MQTTSN_CODEC_VERSION_1_2); + } + }); + } +} diff --git a/mqtt-sn-gateway-paho-connector/src/main/java/org/slj/mqtt/sn/gateway/impl/AggregatingGatewayMain.java b/mqtt-sn-gateway-paho-connector/src/main/java/org/slj/mqtt/sn/gateway/impl/AggregatingGatewayMain.java new file mode 100644 index 00000000..41b3f698 --- /dev/null +++ b/mqtt-sn-gateway-paho-connector/src/main/java/org/slj/mqtt/sn/gateway/impl/AggregatingGatewayMain.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.impl; + +import org.slj.mqtt.sn.codec.MqttsnCodecs; +import org.slj.mqtt.sn.gateway.impl.broker.MqttsnAggregatingBrokerService; +import org.slj.mqtt.sn.gateway.spi.broker.MqttsnBrokerOptions; +import org.slj.mqtt.sn.gateway.spi.gateway.MqttsnGatewayOptions; +import org.slj.mqtt.sn.impl.AbstractMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.model.MqttsnOptions; +import org.slj.mqtt.sn.net.MqttsnUdpOptions; +import org.slj.mqtt.sn.net.MqttsnUdpTransport; + +public class AggregatingGatewayMain { + public static void main(String[] args) throws Exception { + if(args.length < 6) + throw new IllegalArgumentException("you must specify 6 arguments; , , , , and "); + + //-- the local port on which to listen + int localPort = Integer.valueOf(args[0].trim()); + + //-- the clientId of the MQTT broker you are connecting to + String clientId = args[1].trim(); + + //-- the host of the MQTT broker you are connecting to + String host = args[2].trim(); + + //-- the port of the MQTT broker you are connecting to + int port = Integer.valueOf(args[3].trim()); + + //-- the username of the MQTT broker you are connecting to + String username = args[4].trim(); + + //-- the password of the MQTT broker you are connecting to + String password = args[5].trim(); + + MqttsnBrokerOptions brokerOptions = new MqttsnBrokerOptions(). + withHost(host). + withPort(port). + withUsername(username). + withPassword(password); + + //-- configure your gateway runtime + MqttsnOptions gatewayOptions = new MqttsnGatewayOptions(). + withGatewayId(1). + withMaxConnectedClients(10). + withContextId(clientId). + withPredefinedTopic("/my/example/topic/1", 1); + + //-- construct the registry of controllers and config + AbstractMqttsnRuntimeRegistry registry = MqttsnGatewayRuntimeRegistry.defaultConfiguration(gatewayOptions). + withBrokerConnectionFactory(new PahoMqttsnBrokerConnectionFactory()). + withBrokerService(new MqttsnAggregatingBrokerService(brokerOptions)). + withTransport(new MqttsnUdpTransport(new MqttsnUdpOptions().withPort(localPort))). + withCodec(MqttsnCodecs.MQTTSN_CODEC_VERSION_1_2); + + MqttsnGateway gateway = new MqttsnGateway(); + + //-- start the gateway and specify if you wish to join the main gateway thread (blocking) or + //-- specify false to run async if you are embedding + gateway.start(registry, true); + } +} diff --git a/mqtt-sn-gateway-paho-connector/src/main/java/org/slj/mqtt/sn/gateway/impl/PahoMqttsnBrokerConnection.java b/mqtt-sn-gateway-paho-connector/src/main/java/org/slj/mqtt/sn/gateway/impl/PahoMqttsnBrokerConnection.java new file mode 100644 index 00000000..fcd07f18 --- /dev/null +++ b/mqtt-sn-gateway-paho-connector/src/main/java/org/slj/mqtt/sn/gateway/impl/PahoMqttsnBrokerConnection.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.impl; + +import org.eclipse.paho.client.mqttv3.*; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.slj.mqtt.sn.gateway.impl.broker.AbstractMqttsnBrokerConnection; +import org.slj.mqtt.sn.gateway.spi.broker.MqttsnBrokerException; +import org.slj.mqtt.sn.gateway.spi.broker.MqttsnBrokerOptions; +import org.slj.mqtt.sn.model.IMqttsnContext; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Queue; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author simonjohnson + * + * Really simple backend connection to an MQTT broker using the PAHO client library. A single connection is managed by the runtime + * and will be connected either eagerly on startup or lazily according to configuration + */ +public class PahoMqttsnBrokerConnection extends AbstractMqttsnBrokerConnection implements MqttCallback { + + private Logger logger = Logger.getLogger(PahoMqttsnBrokerConnection.class.getName()); + private volatile MqttClient client = null; + private MqttsnBrokerOptions options = null; + private final String clientId; + private Thread publishingThread = null; + private final Object monitor = new Object(); + private final Queue queue = new LinkedList<>(); + private volatile boolean running = false; + + public PahoMqttsnBrokerConnection(MqttsnBrokerOptions options, String clientId) { + this.options = options; + this.clientId = clientId; + } + + public void connect() throws MqttException { + if(client == null || !client.isConnected()){ + synchronized (this){ + if(client == null || !client.isConnected()){ + initClient(); + MqttConnectOptions connectOptions = new MqttConnectOptions(); + connectOptions.setAutomaticReconnect(false); + connectOptions.setPassword(options.getPassword().toCharArray()); + connectOptions.setUserName(options.getUsername()); + connectOptions.setKeepAliveInterval(options.getKeepAlive()); + connectOptions.setConnectionTimeout(options.getConnectionTimeout()); + client.connect(connectOptions); + logger.log(Level.INFO, String.format("connecting client with username [%s] and keepAlive [%s]", options.getUsername(), options.getKeepAlive())); + } + } + } + } + + private void initClient() throws MqttException { + String connectionStr = String.format("%s://%s:%s", options.getProtocol(), options.getHost(), options.getPort()); + client = new MqttClient(connectionStr, clientId, new MemoryPersistence()); + client.setCallback(this); + client.setTimeToWait(options.getConnectionTimeout() * 1000); + logger.log(Level.INFO, String.format("initiated client with host [%s] and clientId [%s]", connectionStr, clientId)); + initPublisher(); + } + + private void initPublisher(){ + running = true; + publishingThread = new Thread(() -> { + do { + try { + if(client != null && client.isConnected()) { + PublishOp op = queue.poll(); + if(op != null){ + logger.log(Level.INFO, String.format("dequeing message to PAHO from queue, [%s] remaining", queue.size())); + client.publish(op.topicPath, op.data, op.QoS, op.retain); + } + } + if(queue.peek() == null) { + synchronized (monitor){ + monitor.wait(); + } + } + } catch(Exception e){ + logger.log(Level.SEVERE, String.format("error publishing via PAHO queue publisher"), e); + } + } while(running); + }, "mqtt-sn-broker-paho-publisher"); + publishingThread.setDaemon(true); + publishingThread.setPriority(Thread.MIN_PRIORITY); + publishingThread.start(); + } + + @Override + public boolean isConnected() throws MqttsnBrokerException { + return client != null && client.isConnected(); + } + + @Override + public void close() { + try { + logger.log(Level.INFO, "closing connection to broker"); + client.disconnect(); + client.close(true); + client = null; + } catch(MqttException e){ + logger.log(Level.SEVERE, "error encountered closing paho client;", e); + } finally { + running = false; + } + } + + @Override + public boolean disconnect(IMqttsnContext context, int keepAlive) throws MqttsnBrokerException { + return true; + } + + public boolean connect(IMqttsnContext context, boolean cleanSession, int keepAlive) throws MqttsnBrokerException{ + return true; + } + + @Override + public boolean subscribe(IMqttsnContext context, String topicPath, int QoS) throws MqttsnBrokerException { + try { + logger.log(Level.INFO, String.format("subscribing connection to [%s] -> [%s]", topicPath, QoS)); + client.subscribe(topicPath, QoS); + return true; + } catch(MqttException e){ + throw new MqttsnBrokerException(e); + } + } + + @Override + public boolean unsubscribe(IMqttsnContext context, String topicPath) throws MqttsnBrokerException { + try { + logger.log(Level.INFO, String.format("unsubscribing connection from [%s]", topicPath)); + client.unsubscribe(topicPath); + return true; + } catch(MqttException e){ + throw new MqttsnBrokerException(e); + } + } + + @Override + public boolean publish(IMqttsnContext context, String topicPath, int QoS, boolean retain, byte[] data) throws MqttsnBrokerException { + try { + PublishOp op = new PublishOp(); + op.data = data; + op.context = context; + op.retain = retain; + op.topicPath = topicPath; + op.QoS = QoS; + queue.add(op); + logger.log(Level.INFO, String.format("queuing message for publish [%s] -> [%s] bytes, queue contains [%s]", topicPath, data.length, queue.size())); + synchronized (monitor){ + monitor.notify(); + } + return true; + } catch(Exception e){ + throw new MqttsnBrokerException(e); + } + } + + @Override + public void connectionLost(Throwable t) { + logger.log(Level.SEVERE, "connection reported lost on broker side", t); + try { + client.close(true); + } catch(Exception e){ + } finally { + client = null; + running = false; + } + } + + @Override + public void messageArrived(String s, MqttMessage mqttMessage) throws Exception { + try { + byte[] data = mqttMessage.getPayload(); + logger.log(Level.INFO, String.format("recieved message from connection [%s] -> [%s] bytes", s, data.length)); + receive(s, data, mqttMessage.getQos()); + } catch(Exception e){ + logger.log(Level.SEVERE, "gateway reported issue receiving message from broker;", e); + } + } + + @Override + public void deliveryComplete(IMqttDeliveryToken token) { + if(logger.isLoggable(Level.FINE)){ + logger.log(Level.FINE, String.format("broker confirm delivery complete [%s] -> [%s]", + token.getMessageId(), Arrays.toString(token.getTopics()))); + } + } + + static class PublishOp { + public IMqttsnContext context; + public String topicPath; + public int QoS; + public boolean retain; + public byte[] data; + } +} \ No newline at end of file diff --git a/mqtt-sn-gateway-paho-connector/src/main/java/org/slj/mqtt/sn/gateway/impl/PahoMqttsnBrokerConnectionFactory.java b/mqtt-sn-gateway-paho-connector/src/main/java/org/slj/mqtt/sn/gateway/impl/PahoMqttsnBrokerConnectionFactory.java new file mode 100644 index 00000000..215a7e4c --- /dev/null +++ b/mqtt-sn-gateway-paho-connector/src/main/java/org/slj/mqtt/sn/gateway/impl/PahoMqttsnBrokerConnectionFactory.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.impl; + +import org.slj.mqtt.sn.gateway.spi.broker.IMqttsnBrokerConnectionFactory; +import org.slj.mqtt.sn.gateway.spi.broker.MqttsnBrokerException; +import org.slj.mqtt.sn.gateway.spi.broker.MqttsnBrokerOptions; + +public class PahoMqttsnBrokerConnectionFactory implements IMqttsnBrokerConnectionFactory { + + @Override + public PahoMqttsnBrokerConnection createConnection(MqttsnBrokerOptions options, String clientId) throws MqttsnBrokerException { + try { + PahoMqttsnBrokerConnection connection = new PahoMqttsnBrokerConnection(options, clientId); + connection.connect(); + return connection; + } catch(Exception e){ + throw new MqttsnBrokerException("error creating connection;", e); + } + } +} diff --git a/mqtt-sn-gateway/pom.xml b/mqtt-sn-gateway/pom.xml new file mode 100644 index 00000000..d6f13ea1 --- /dev/null +++ b/mqtt-sn-gateway/pom.xml @@ -0,0 +1,71 @@ + + + + + 4.0.0 + + org.slj + mqtt-sn-gateway + 1.0.0 + + + UTF-8 + 4.13 + 3.8.1 + 1.8 + 1.8 + + + + + org.slj + mqtt-sn-codec + 1.0.0 + + + org.slj + mqtt-sn-core + 1.0.0 + + + junit + junit + ${junit.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + \ No newline at end of file diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/cli/MqttsnInteractiveGateway.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/cli/MqttsnInteractiveGateway.java new file mode 100644 index 00000000..43fe1f2c --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/cli/MqttsnInteractiveGateway.java @@ -0,0 +1,351 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.gateway.cli; + +import org.slj.mqtt.sn.cli.AbstractInteractiveCli; +import org.slj.mqtt.sn.gateway.impl.MqttsnGateway; +import org.slj.mqtt.sn.gateway.impl.MqttsnGatewayRuntimeRegistry; +import org.slj.mqtt.sn.gateway.spi.gateway.MqttsnGatewayOptions; +import org.slj.mqtt.sn.impl.AbstractMqttsnRuntime; +import org.slj.mqtt.sn.impl.AbstractMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.impl.AbstractMqttsnUdpTransport; +import org.slj.mqtt.sn.model.*; +import org.slj.mqtt.sn.net.MqttsnUdpOptions; +import org.slj.mqtt.sn.net.MqttsnUdpTransport; +import org.slj.mqtt.sn.spi.IMqttsnTransport; +import org.slj.mqtt.sn.spi.MqttsnException; +import org.slj.mqtt.sn.spi.NetworkRegistryException; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; + +public abstract class MqttsnInteractiveGateway extends AbstractInteractiveCli { + + static final String USERNAME = "username"; + static final String PASSWORD = "password"; + + protected String username; + protected String password; + + enum COMMANDS { + NETWORK("View network registry", new String[0]), + STATS("View statistics for runtime", new String[0]), + RESET("Reset the stats", new String[0]), + QUEUE("Queue a new message for clients", new String[]{"String* topicName, String* payload, int QoS"}), + SESSION("Obtain the status of a client", new String[]{"String* clientId"}), + STATUS("Obtain the status of the runtime", new String[0]), + PREDEFINE("Add a predefined topic alias", new String[]{"String* topicName", "int16 topicAlias"}), + HELP("List this message", new String[0]), + QUIT("Quit the application", new String[0]), + EXIT("Quit the application", new String[0], true), + BYE("Quit the application", new String[0], true); + + private String description; + private String[] arguments; + private boolean hidden = false; + + COMMANDS(String description, String[] arguments, boolean hidden){ + this(description, arguments); + this.hidden = hidden; + } + + COMMANDS(String description, String[] arguments){ + this.description = description; + this.arguments = arguments; + } + + public boolean isHidden() { + return hidden; + } + + public String[] getArguments() { + return arguments; + } + + public String getDescription(){ + return description; + } + } + + @Override + protected boolean processCommand(String command) throws Exception { + COMMANDS c = COMMANDS.valueOf(command.toUpperCase()); + processCommand(c); + if(c == COMMANDS.QUIT || c == COMMANDS.BYE || c == COMMANDS.EXIT){ + return false; + } + return true; + } + + protected void processCommand(COMMANDS command) throws Exception { + try { + switch (command){ + case HELP: + for(COMMANDS c : COMMANDS.values()){ + if(c.isHidden()) continue; + StringBuilder sb = new StringBuilder(); + for(String a : c.getArguments()){ + if(sb.length() > 0){ + sb.append(", "); + } + sb.append(a); + } + output.println("\t" + c.name()); + output.println("\t\t" + c.getDescription()); + } + break; + case SESSION: + session(captureMandatoryString(input, output, "Please supply the clientId whose session you would like to see")); + break; + case NETWORK: + network(); + break; + case STATS: + stats(); + break; + case RESET: + resetMetrics(); + break; + case STATUS: + status(); + break; + case PREDEFINE: + predefine( + captureMandatoryString(input, output, "What is the topic you would like to predefine?"), + captureMandatoryInt(input, output, "What is the alias for the topic?", null)); + break; + case QUEUE: + queue( + captureMandatoryString(input, output, "What is the topic you would like to publish to?"), + captureMandatoryString(input, output, "Supply the data to publish"), + captureMandatoryInt(input, output, "What is QoS for the publish?", new int[]{0,1,2})); + break; + case EXIT: + case BYE: + case QUIT: + quit(); + break; + } + } catch(Exception e){ + error( "An error occurred running your command.", e); + } + } + + protected void queue(String topicName, String payload, int QoS) + throws IOException, MqttsnException { + + message("Enqueued publish to all subscribed sessions: " + topicName); + MqttsnGatewayRuntimeRegistry gatewayRuntimeRegistry = (MqttsnGatewayRuntimeRegistry) getRuntimeRegistry(); + gatewayRuntimeRegistry.getGatewaySessionService().receiveToSessions(topicName, payload.getBytes(StandardCharsets.UTF_8), QoS); + } + + protected void network() throws IOException, MqttsnException, NetworkRegistryException { + message("Network registry: "); + MqttsnGatewayRuntimeRegistry gatewayRuntimeRegistry = (MqttsnGatewayRuntimeRegistry) getRuntimeRegistry(); + Iterator itr = gatewayRuntimeRegistry.getNetworkRegistry().iterator(); + while(itr.hasNext()){ + INetworkContext c = itr.next(); + message("\t" + c + " -> " + gatewayRuntimeRegistry.getNetworkRegistry().hasBoundSessionContext(c)); + } + } + + protected void session(String clientId) + throws IOException, MqttsnException { + MqttsnGatewayOptions opts = (MqttsnGatewayOptions) getOptions(); + MqttsnGatewayRuntimeRegistry gatewayRuntimeRegistry = (MqttsnGatewayRuntimeRegistry) getRuntimeRegistry(); + Optional context = + gatewayRuntimeRegistry.getGatewaySessionService().lookupClientIdSession(clientId); + if(context.isPresent()){ + IMqttsnContext c = context.get(); + IMqttsnSessionState state = gatewayRuntimeRegistry. + getGatewaySessionService().getSessionState(c, false); + + message("Client session: " + clientId); + message("Session started: " + format(state.getSessionStarted())); + message("Last seen: " + format(state.getLastSeen())); + message("Keep alive (seconds): " + state.getKeepAlive()); + message("Session length (seconds): " + ((System.currentTimeMillis() - state.getSessionStarted().getTime()) / 1000)); + message("State: " + getColorForState(state.getClientState()) + state.getClientState().name()); + message("Queue size: " + gatewayRuntimeRegistry.getMessageQueue().size(c)); + + Set subs = gatewayRuntimeRegistry.getSubscriptionRegistry().readSubscriptions(c); + Iterator itr = subs.iterator(); + message("Subscription(s): "); + synchronized (subs){ + while(itr.hasNext()){ + Subscription s = itr.next(); + message("\t" + s.getTopicPath() + " -> " + s.getQoS()); + } + } + + INetworkContext networkContext = gatewayRuntimeRegistry.getNetworkRegistry().getContext(c); + message("Network Address(s): " + networkContext.getNetworkAddress()); + + } else { + message("No session found: " + clientId); + } + } + + protected void status() + throws IOException, MqttsnException { + MqttsnGatewayOptions opts = (MqttsnGatewayOptions) getOptions(); + MqttsnGatewayRuntimeRegistry gatewayRuntimeRegistry = (MqttsnGatewayRuntimeRegistry) getRuntimeRegistry(); + if(runtime != null) { + + boolean connected = gatewayRuntimeRegistry.getBrokerService().isConnected(null); + + int maxClients = opts.getMaxConnectedClients(); + int advertiseTime = opts.getGatewayAdvertiseTime(); + + //-- general stuff + message("Gateway Id: " + opts.getGatewayId()); + message("Advertise Interval: " + advertiseTime); + + if(gatewayRuntimeRegistry.getTransport() instanceof AbstractMqttsnUdpTransport){ + MqttsnUdpOptions udpOptions = ((AbstractMqttsnUdpTransport)gatewayRuntimeRegistry.getTransport()).getUdpOptions(); + message("Host: " + udpOptions.getHost()); + message("Datagram port: " + udpOptions.getPort()); + message("Secure port: " + udpOptions.getSecurePort()); + message("Broadcast port: " + udpOptions.getBroadcastPort()); + message("MTU: " + udpOptions.getMtu()); + } + + message("Max message size: " + getOptions().getMaxProtocolMessageSize()); + message("Max connected clients: " + maxClients); + + if (getOptions() != null) { + Map pTopics = getOptions().getPredefinedTopics(); + if(pTopics != null){ + message( "Predefined topic count: " + pTopics.size()); + Iterator itr = pTopics.keySet().iterator(); + while(itr.hasNext()){ + String topic = itr.next(); + message( "\t" + topic + " = " + pTopics.get(topic)); + } + } + } + + Iterator sessionItr = gatewayRuntimeRegistry.getGatewaySessionService().iterator(); + List allState = new ArrayList<>(); + + int queuedMessages = 0; + while(sessionItr.hasNext()){ + IMqttsnContext c = sessionItr.next(); + IMqttsnSessionState state = gatewayRuntimeRegistry.getGatewaySessionService(). + getSessionState(c, false); + allState.add(state); + queuedMessages += gatewayRuntimeRegistry.getMessageQueue().size(c); + } + + message("Currently connected clients: " + allState.size()); + message("Current buffered messages: " + queuedMessages); + + //-- broker stuff +// message("Broker hostname: " + hostName); +// message("Broker port: " + port); + message("Broker TCP/IP Connection State: " + (connected ? cli_green() + "ESTABLISHED" : cli_red() + "UNESTABLISHED")); + + } else { + message( "Gateway status: awaiting connection.."); + } + } + + @Override + protected MqttsnOptions createOptions() throws UnknownHostException { + return new MqttsnGatewayOptions(). + withGatewayId(101). + withContextId(clientId). + withMaxMessagesInQueue(100). + withMinFlushTime(400). + withSleepClearsRegistrations(false); + } + + @Override + protected IMqttsnTransport createTransport() { + MqttsnUdpOptions udpOptions = new MqttsnUdpOptions(). + withPort(MqttsnUdpOptions.DEFAULT_LOCAL_PORT); + return new MqttsnUdpTransport(udpOptions); + } + + @Override + protected AbstractMqttsnRuntime createRuntime(AbstractMqttsnRuntimeRegistry registry, MqttsnOptions options) { + MqttsnGateway gateway = new MqttsnGateway(); + return gateway; + } + + @Override + public void start() throws Exception { + super.start(); + getRuntime().start(getRuntimeRegistry(), false); + } + + @Override + protected void configure() throws IOException { + super.configure(); + username = captureMandatoryString(input, output, "Please enter a valid username for you broker connection"); + password = captureMandatoryString(input, output, "Please enter a valid password for you broker connection"); + } + + @Override + protected void loadConfigHistory(Properties props) throws IOException { + super.loadConfigHistory(props); + username = props.getProperty(USERNAME); + password = props.getProperty(PASSWORD); + } + + @Override + protected void saveConfigHistory(Properties props) { + super.saveConfigHistory(props); + props.setProperty(USERNAME, username); + props.setProperty(PASSWORD, password); + } + + @Override + protected String getPropertyFileName() { + return "gateway.properties"; + } + + private static String format(Date d){ + SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); + return sdf.format(d); + } + + public String getColorForState(MqttsnClientState state){ + if(state == null) return cli_reset(); + switch(state){ + case AWAKE: + case CONNECTED: return cli_green(); + case ASLEEP: return cli_blue(); + case PENDING: return cli_reset(); + default: return cli_red(); + } + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/cli/MqttsnInteractiveGatewayLauncher.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/cli/MqttsnInteractiveGatewayLauncher.java new file mode 100644 index 00000000..6d822b1c --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/cli/MqttsnInteractiveGatewayLauncher.java @@ -0,0 +1,53 @@ +/* + * + * * Copyright (c) 2021 Simon Johnson + * * + * * Find me on GitHub: + * * https://github.com/simon622 + * * + * * Licensed to the Apache Software Foundation (ASF) under one + * * or more contributor license agreements. See the NOTICE file + * * distributed with this work for additional information + * * regarding copyright ownership. The ASF licenses this file + * * to you under the Apache License, Version 2.0 (the + * * "License"); you may not use this file except in compliance + * * with the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * + * + */ + +package org.slj.mqtt.sn.gateway.cli; + +import java.io.PrintStream; +import java.util.Scanner; +import java.util.logging.LogManager; + +public class MqttsnInteractiveGatewayLauncher { + public static void launch(MqttsnInteractiveGateway interactiveGateway) throws Exception { + if(!Boolean.getBoolean("debug")) LogManager.getLogManager().reset(); + Scanner input = new Scanner(System.in); + PrintStream output = System.out; + try { + interactiveGateway.init(input, output); + interactiveGateway.welcome(); + interactiveGateway.configureWithHistory(); + interactiveGateway.start(); + interactiveGateway.command(); + interactiveGateway.exit(); + } catch(Exception e){ + System.err.println("A fatal error was encountered: " + e.getMessage()); + } finally { + input.close(); + interactiveGateway.stop(); + } + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/MqttsnGateway.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/MqttsnGateway.java new file mode 100644 index 00000000..4667d113 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/MqttsnGateway.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.impl; + +import org.slj.mqtt.sn.gateway.spi.gateway.IMqttsnGatewayRuntimeRegistry; +import org.slj.mqtt.sn.impl.AbstractMqttsnRuntime; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.spi.IMqttsnPublishReceivedListener; +import org.slj.mqtt.sn.spi.IMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.spi.MqttsnException; + +import java.io.IOException; +import java.util.logging.Level; + +public class MqttsnGateway extends AbstractMqttsnRuntime { + + protected void startupServices(IMqttsnRuntimeRegistry runtime) throws MqttsnException { + + //-- ensure we start all the startable services + callStartup(runtime.getMessageHandler()); + callStartup(runtime.getMessageQueue()); + callStartup(runtime.getMessageRegistry()); + callStartup(runtime.getTopicRegistry()); + callStartup(runtime.getSubscriptionRegistry()); + callStartup(runtime.getMessageStateService()); + callStartup(runtime.getQueueProcessorStateCheckService()); + callStartup(runtime.getQueueProcessor()); + callStartup(runtime.getContextFactory()); + if (runtime.getPermissionService() != null) callStartup(runtime.getPermissionService()); + + //-- start the network last + callStartup(((IMqttsnGatewayRuntimeRegistry) runtime).getGatewaySessionService()); + callStartup(((IMqttsnGatewayRuntimeRegistry) runtime).getBrokerConnectionFactory()); + callStartup(((IMqttsnGatewayRuntimeRegistry) runtime).getBrokerService()); + + //-- start discovery + if (runtime.getOptions().isEnableDiscovery()) { + callStartup(((IMqttsnGatewayRuntimeRegistry) runtime).getGatewayAdvertiseService()); + } + + callStartup(runtime.getTransport()); + + //-- notify the broker of confirmed messahe + registerReceivedListener(new IMqttsnPublishReceivedListener() { + @Override + public void receive(IMqttsnContext context, String topicName, int QoS, byte[] data) { + try { + ((IMqttsnGatewayRuntimeRegistry) registry).getBrokerService().publish(context, topicName, QoS, data); + } catch (MqttsnException e) { + logger.log(Level.SEVERE, "error publishing message to broker", e); + } + } + }); + } + + public void stopServices(IMqttsnRuntimeRegistry runtime) throws MqttsnException { + + //-- stop the networks first + callShutdown(runtime.getTransport()); + + if (runtime.getOptions().isEnableDiscovery()) { + callShutdown(((IMqttsnGatewayRuntimeRegistry) runtime).getGatewayAdvertiseService()); + } + + callShutdown(((IMqttsnGatewayRuntimeRegistry) runtime).getGatewaySessionService()); + callShutdown(((IMqttsnGatewayRuntimeRegistry) runtime).getBrokerConnectionFactory()); + callShutdown(((IMqttsnGatewayRuntimeRegistry) runtime).getBrokerService()); + + //-- ensure we stop all the startable services + + if (runtime.getPermissionService() != null) callShutdown(runtime.getPermissionService()); + callShutdown(runtime.getContextFactory()); + callShutdown(runtime.getMessageHandler()); + callShutdown(runtime.getMessageQueue()); + callShutdown(runtime.getMessageRegistry()); + callShutdown(runtime.getTopicRegistry()); + callShutdown(runtime.getSubscriptionRegistry()); + callShutdown(runtime.getQueueProcessorStateCheckService()); + callShutdown(runtime.getQueueProcessor()); + callShutdown(runtime.getMessageStateService()); + } + + public boolean handleRemoteDisconnect(IMqttsnContext context) { + return true; + } + + public boolean handleLocalDisconnectError(IMqttsnContext context, Throwable t) { + return true; + } + + @Override + public void close() throws IOException { + try { + stop(); + } catch(Exception e){ + throw new IOException(e); + } + } +} \ No newline at end of file diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/MqttsnGatewayRuntimeRegistry.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/MqttsnGatewayRuntimeRegistry.java new file mode 100644 index 00000000..4b45d571 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/MqttsnGatewayRuntimeRegistry.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.impl; + +import org.slj.mqtt.sn.gateway.impl.gateway.*; +import org.slj.mqtt.sn.gateway.spi.broker.IMqttsnBrokerConnectionFactory; +import org.slj.mqtt.sn.gateway.spi.broker.IMqttsnBrokerService; +import org.slj.mqtt.sn.gateway.spi.gateway.IMqttsnGatewayAdvertiseService; +import org.slj.mqtt.sn.gateway.spi.gateway.IMqttsnGatewayRuntimeRegistry; +import org.slj.mqtt.sn.gateway.spi.gateway.IMqttsnGatewaySessionRegistryService; +import org.slj.mqtt.sn.impl.*; +import org.slj.mqtt.sn.impl.ram.*; +import org.slj.mqtt.sn.model.MqttsnOptions; +import org.slj.mqtt.sn.net.NetworkAddressRegistry; +import org.slj.mqtt.sn.spi.MqttsnRuntimeException; + +public class MqttsnGatewayRuntimeRegistry extends AbstractMqttsnRuntimeRegistry implements IMqttsnGatewayRuntimeRegistry { + + private IMqttsnGatewayAdvertiseService advertiseService; + private IMqttsnBrokerService brokerService; + private IMqttsnBrokerConnectionFactory connectionFactory; + private IMqttsnGatewaySessionRegistryService sessionService; + + public MqttsnGatewayRuntimeRegistry(MqttsnOptions options){ + super(options); + } + + public static MqttsnGatewayRuntimeRegistry defaultConfiguration(MqttsnOptions options){ + MqttsnGatewaySessionService sessionService = new MqttsnGatewaySessionService(); + final MqttsnGatewayRuntimeRegistry registry = (MqttsnGatewayRuntimeRegistry) new MqttsnGatewayRuntimeRegistry(options). + withGatewaySessionService(new MqttsnGatewaySessionService()). + withGatewayAdvertiseService(new MqttsnGatewayAdvertiseService()). + withMessageHandler(new MqttsnGatewayMessageHandler()). + withMessageRegistry(new MqttsnInMemoryMessageRegistry()). + withNetworkAddressRegistry(new NetworkAddressRegistry(options.getMaxNetworkAddressEntries())). + withMessageQueue(new MqttsnInMemoryMessageQueue()). + withContextFactory(new MqttsnContextFactory()). + withTopicRegistry(new MqttsnInMemoryTopicRegistry()). + withQueueProcessor(new MqttsnMessageQueueProcessor(false)). + withQueueProcessorStateCheck(new MqttsnGatewayQueueProcessorStateService()). + withSubscriptionRegistry(new MqttsnInMemorySubscriptionRegistry()). + withPermissionService(new MqttsnGatewayPermissionService()). + withMessageStateService(new MqttsnInMemoryMessageStateService(false)); + return registry; + } + + public MqttsnGatewayRuntimeRegistry withGatewayAdvertiseService(IMqttsnGatewayAdvertiseService advertiseService){ + this.advertiseService = advertiseService; + return this; + } + + public MqttsnGatewayRuntimeRegistry withBrokerConnectionFactory(IMqttsnBrokerConnectionFactory connectionFactory){ + this.connectionFactory = connectionFactory; + return this; + } + + public MqttsnGatewayRuntimeRegistry withGatewaySessionService(IMqttsnGatewaySessionRegistryService sessionService){ + this.sessionService = sessionService; + return this; + } + + public MqttsnGatewayRuntimeRegistry withBrokerService(IMqttsnBrokerService brokerService){ + this.brokerService = brokerService; + return this; + } + + @Override + public IMqttsnGatewaySessionRegistryService getGatewaySessionService() { + return sessionService; + } + + @Override + public IMqttsnBrokerService getBrokerService() { + return brokerService; + } + + @Override + public IMqttsnBrokerConnectionFactory getBrokerConnectionFactory() { + return connectionFactory; + } + + public IMqttsnGatewayAdvertiseService getGatewayAdvertiseService() { + return advertiseService; + } + + @Override + protected void validateOnStartup() throws MqttsnRuntimeException { + if(brokerService == null) throw new MqttsnRuntimeException("message state service must be bound for valid runtime"); + if(connectionFactory == null) throw new MqttsnRuntimeException("connection factory must be bound for valid runtime"); + if(sessionService == null) throw new MqttsnRuntimeException("session service must be bound for valid runtime"); + } +} \ No newline at end of file diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/broker/AbstractMqttsnBrokerConnection.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/broker/AbstractMqttsnBrokerConnection.java new file mode 100644 index 00000000..618b095f --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/broker/AbstractMqttsnBrokerConnection.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.impl.broker; + +import org.slj.mqtt.sn.gateway.spi.broker.IMqttsnBrokerConnection; +import org.slj.mqtt.sn.gateway.spi.broker.IMqttsnBrokerService; +import org.slj.mqtt.sn.spi.MqttsnException; + +/** + * Allow the connection to be bootstrapped so implementers can simply extend this to provide their callbacks + */ +public abstract class AbstractMqttsnBrokerConnection implements IMqttsnBrokerConnection { + + protected IMqttsnBrokerService brokerService; + + public void setBrokerService(IMqttsnBrokerService brokerService){ + this.brokerService = brokerService; + } + + public void receive(String topicPath, byte[] payload, int QoS) throws MqttsnException { + if(brokerService == null){ + throw new MqttsnException("brokerService not available to connection, receive will fail"); + } + brokerService.receive(topicPath, payload, QoS); + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/broker/AbstractMqttsnBrokerService.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/broker/AbstractMqttsnBrokerService.java new file mode 100644 index 00000000..907d72b3 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/broker/AbstractMqttsnBrokerService.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.impl.broker; + +import org.slj.mqtt.sn.gateway.spi.*; +import org.slj.mqtt.sn.gateway.spi.broker.IMqttsnBrokerConnection; +import org.slj.mqtt.sn.gateway.spi.broker.IMqttsnBrokerService; +import org.slj.mqtt.sn.gateway.spi.broker.MqttsnBrokerException; +import org.slj.mqtt.sn.gateway.spi.broker.MqttsnBrokerOptions; +import org.slj.mqtt.sn.gateway.spi.gateway.IMqttsnGatewayRuntimeRegistry; +import org.slj.mqtt.sn.impl.AbstractMqttsnBackoffThreadService; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.spi.MqttsnException; +import org.slj.mqtt.sn.spi.MqttsnRuntimeException; + +import java.util.logging.Level; + +public abstract class AbstractMqttsnBrokerService + extends AbstractMqttsnBackoffThreadService implements IMqttsnBrokerService { + + protected MqttsnBrokerOptions options; + + public AbstractMqttsnBrokerService(MqttsnBrokerOptions options){ + this.options = options; + } + + @Override + public void start(IMqttsnGatewayRuntimeRegistry runtime) throws MqttsnException { + super.start(runtime); + validateBrokerConnectionDetails(); + if(options.getConnectOnStartup()){ + logger.log(Level.INFO, "connect during startup requested.."); + try { + getBrokerConnection(null); + } catch(MqttsnBrokerException e){ + logger.log(Level.SEVERE, "encountered error attempting broker connect..", e); + throw new MqttsnException("encountered error attempting broker connect..",e); + } + logger.log(Level.INFO, "connection complete, broker service ready."); + } + } + + protected void validateBrokerConnectionDetails(){ + if(!options.validConnectionDetails()){ + throw new MqttsnRuntimeException("invalid broker connection details!"); + } + } + + @Override + public ConnectResult connect(IMqttsnContext context, String clientId, boolean cleanSession, int keepAlive) throws MqttsnBrokerException { + IMqttsnBrokerConnection connection = getBrokerConnection(context); + if(!connection.isConnected()){ + throw new MqttsnBrokerException("underlying broker connection was not connected"); + } + boolean success = connection.connect(context, cleanSession, keepAlive); + return new ConnectResult(success ? Result.STATUS.SUCCESS : Result.STATUS.ERROR, success ? "connection success" : "connection refused by broker side"); + } + + @Override + public DisconnectResult disconnect(IMqttsnContext context, int keepAlive) throws MqttsnBrokerException { + IMqttsnBrokerConnection connection = getBrokerConnection(context); + if(!connection.isConnected()){ + throw new MqttsnBrokerException("underlying broker connection was not connected"); + } + boolean success = connection.disconnect(context, keepAlive); + return new DisconnectResult(success ? Result.STATUS.SUCCESS : Result.STATUS.ERROR, success ? "disconnection success" : "disconnection refused by broker side"); + } + + @Override + public PublishResult publish(IMqttsnContext context, String topicPath, int QoS, byte[] payload) throws MqttsnBrokerException { + IMqttsnBrokerConnection connection = getBrokerConnection(context); + if(!connection.isConnected()){ + throw new MqttsnBrokerException("underlying broker connection was not connected"); + } + boolean success = connection.publish(context, topicPath, QoS, false, payload); + return new PublishResult(success ? Result.STATUS.SUCCESS : Result.STATUS.ERROR, success ? "publish success" : "publish refused by broker side"); + } + + @Override + public SubscribeResult subscribe(IMqttsnContext context, String topicPath, int QoS) throws MqttsnBrokerException { + IMqttsnBrokerConnection connection = getBrokerConnection(context); + if(!connection.isConnected()){ + throw new MqttsnBrokerException("underlying broker connection was not connected"); + } + boolean success = connection.subscribe(context, topicPath, QoS); + SubscribeResult res = new SubscribeResult(success ? Result.STATUS.SUCCESS : Result.STATUS.ERROR); + if(success) res.setGrantedQoS(QoS); + return res; + } + + @Override + public UnsubscribeResult unsubscribe(IMqttsnContext context, String topicPath) throws MqttsnBrokerException { + IMqttsnBrokerConnection connection = getBrokerConnection(context); + if(!connection.isConnected()){ + throw new MqttsnBrokerException("underlying broker connection was not connected"); + } + boolean success = connection.unsubscribe(context, topicPath); + return new UnsubscribeResult(success ? Result.STATUS.SUCCESS : Result.STATUS.ERROR); + } + + protected IMqttsnBrokerConnection getBrokerConnection(IMqttsnContext context) throws MqttsnBrokerException{ + synchronized (this){ + IMqttsnBrokerConnection connection = getBrokerConnectionInternal(context); + if(!connection.isConnected()){ + throw new MqttsnBrokerException("underlying broker connection was not connected"); + } + return connection; + } + } + + @Override + public void receive(String topicPath, byte[] payload, int QoS) throws MqttsnException { + registry.getGatewaySessionService().receiveToSessions(topicPath, payload, QoS); + } + + protected abstract void close(IMqttsnBrokerConnection connection) throws MqttsnBrokerException; + + protected abstract IMqttsnBrokerConnection getBrokerConnectionInternal(IMqttsnContext context) throws MqttsnBrokerException; +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/broker/MqttsnAggregatingBrokerService.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/broker/MqttsnAggregatingBrokerService.java new file mode 100644 index 00000000..f8d2389e --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/broker/MqttsnAggregatingBrokerService.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.impl.broker; + +import org.slj.mqtt.sn.gateway.spi.broker.IMqttsnBrokerConnection; +import org.slj.mqtt.sn.gateway.spi.broker.MqttsnBrokerException; +import org.slj.mqtt.sn.gateway.spi.broker.MqttsnBrokerOptions; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.spi.MqttsnException; + +import java.util.logging.Level; + +/** + * A single broker connection is maintained and used for all connecting gateway side + * devices + */ +public class MqttsnAggregatingBrokerService extends AbstractMqttsnBrokerService { + + volatile IMqttsnBrokerConnection connection; + volatile boolean stopped = false; + + public MqttsnAggregatingBrokerService(MqttsnBrokerOptions options){ + super(options); + } + + @Override + public void stop() throws MqttsnException { + stopped = true; + super.stop(); + try { + close(connection); + } catch(MqttsnBrokerException e){ + logger.log(Level.WARNING, "error encountered shutting down broker connection;", e); + } + } + + @Override + protected void initThread() { + //-- only start deamon process if we are managing the connections + if(options.getManagedConnections()){ + super.initThread(); + } + } + + @Override + public boolean isConnected(IMqttsnContext context) throws MqttsnBrokerException { + return !stopped && connection != null && connection.isConnected(); + } + + @Override + protected long doWork() { + try { + if(options.getManagedConnections()){ + logger.log(Level.FINE, "checking status of managed connection.."); + if(connection != null){ + if(!connection.isConnected()){ + logger.log(Level.WARNING, "detected invalid connection to broker, dropping stale connection."); + close(connection); + } + } else { + initConnection(); + } + } else { + if(options.getConnectOnStartup() && connection == null){ + initConnection(); + } + } + } catch(Exception e){ + logger.log(Level.SEVERE, "error occurred monitoring connections;", e); + } + + return 10000; + } + + @Override + protected IMqttsnBrokerConnection getBrokerConnectionInternal(IMqttsnContext context) throws MqttsnBrokerException { + if(stopped) throw new MqttsnBrokerException("broker service is in the process or shutting down"); + initConnection(); + return connection; + } + + protected void initConnection() throws MqttsnBrokerException { + if(connection == null){ + //-- in aggregation mode connect with the gatewayId as the clientId on the broker side + synchronized (this){ + if(connection == null){ + connection = registry.getBrokerConnectionFactory().createConnection(options, + registry.getOptions().getContextId()); + //-- bootstrap to recieve events from the connections.. + //-- TODO - I dont like this pattern + if(connection instanceof AbstractMqttsnBrokerConnection){ + ((AbstractMqttsnBrokerConnection)connection).setBrokerService(this); + } + } + } + } + } + + @Override + protected void close(IMqttsnBrokerConnection connection) throws MqttsnBrokerException { + if(connection != null && connection.isConnected()){ + connection.close(); + } + this.connection = null; + } + + @Override + protected String getDaemonName() { + return "gateway-broker-managed-connection"; + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/gateway/MqttsnGatewayAdvertiseService.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/gateway/MqttsnGatewayAdvertiseService.java new file mode 100644 index 00000000..72895b81 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/gateway/MqttsnGatewayAdvertiseService.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.impl.gateway; + +import org.slj.mqtt.sn.gateway.spi.gateway.IMqttsnGatewayAdvertiseService; +import org.slj.mqtt.sn.gateway.spi.gateway.MqttsnGatewayOptions; +import org.slj.mqtt.sn.impl.AbstractMqttsnBackoffThreadService; +import org.slj.mqtt.sn.spi.IMqttsnMessage; +import org.slj.mqtt.sn.spi.IMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.spi.MqttsnException; + +import java.util.logging.Level; + +public class MqttsnGatewayAdvertiseService extends AbstractMqttsnBackoffThreadService implements IMqttsnGatewayAdvertiseService { + + long lastGatewayBroadcastTime; + + public synchronized void start(IMqttsnRuntimeRegistry runtime) throws MqttsnException { + if(runtime.getOptions().isEnableDiscovery()){ + super.start(runtime); + } + } + + @Override + protected long doWork() { + int timeout = 0; + try { + timeout = ((MqttsnGatewayOptions)registry.getOptions()).getGatewayAdvertiseTime(); + int gatewayId = ((MqttsnGatewayOptions) registry.getOptions()).getGatewayId(); + + logger.log(Level.INFO, String.format("advertising gateway id [%s], next sending time in [%s] seconds", + gatewayId, timeout)); + + IMqttsnMessage msg = registry.getMessageFactory().createAdvertise( + ((MqttsnGatewayOptions) registry.getOptions()).getGatewayId(), + timeout); + registry.getTransport().broadcast(msg); + lastGatewayBroadcastTime = System.currentTimeMillis(); + + } catch(Exception e){ + logger.log(Level.WARNING, String.format("error sending advertising message"), e); + } + + return timeout * 1000; + } + + @Override + protected String getDaemonName() { + return "gateway-advertise"; + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/gateway/MqttsnGatewayMessageHandler.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/gateway/MqttsnGatewayMessageHandler.java new file mode 100644 index 00000000..b910da6f --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/gateway/MqttsnGatewayMessageHandler.java @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.impl.gateway; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.codec.MqttsnCodecException; +import org.slj.mqtt.sn.gateway.spi.*; +import org.slj.mqtt.sn.gateway.spi.gateway.IMqttsnGatewayRuntimeRegistry; +import org.slj.mqtt.sn.gateway.spi.gateway.MqttsnGatewayOptions; +import org.slj.mqtt.sn.impl.AbstractMqttsnMessageHandler; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.IMqttsnSessionState; +import org.slj.mqtt.sn.model.MqttsnClientState; +import org.slj.mqtt.sn.model.TopicInfo; +import org.slj.mqtt.sn.spi.IMqttsnMessage; +import org.slj.mqtt.sn.spi.MqttsnException; +import org.slj.mqtt.sn.utils.MqttsnUtils; +import org.slj.mqtt.sn.wire.MqttsnWireUtils; +import org.slj.mqtt.sn.wire.version1_2.payload.*; + +import java.util.Set; +import java.util.logging.Level; + +public class MqttsnGatewayMessageHandler + extends AbstractMqttsnMessageHandler { + + protected IMqttsnSessionState getSessionState(IMqttsnContext context) throws MqttsnException, MqttsnInvalidSessionStateException { + IMqttsnSessionState state = registry.getGatewaySessionService().getSessionState(context, false); + if(state == null || state.getClientState() == MqttsnClientState.DISCONNECTED) + throw new MqttsnInvalidSessionStateException("session not available for context"); + return state; + } + + protected IMqttsnSessionState getSessionState(IMqttsnContext context, boolean createIfNotExists) throws MqttsnException { + IMqttsnSessionState state = registry.getGatewaySessionService().getSessionState(context, createIfNotExists); + return state; + } + + @Override + protected void beforeHandle(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException { + + try { + //see if the client has an active sessionx + getSessionState(context); + } catch(MqttsnInvalidSessionStateException e){ + + //if they do NOT, the only time we can process messages + //on their behalf is if its a CONNECT or a PUBLISH M 1 + boolean shouldContinue = false; + if(message instanceof MqttsnConnect){ + //this is ok + shouldContinue = true; + } else if(message instanceof MqttsnPublish){ + MqttsnPublish p = (MqttsnPublish) message; + if(p.getQoS() == MqttsnConstants.QoSM1){ + //this is ok + shouldContinue = true; + } + } else if(message instanceof MqttsnDisconnect){ + shouldContinue = true; + } + if(!shouldContinue){ + logger.log(Level.WARNING, String.format("detected invalid client session state for [%s] and inbound message [%s]", context, message)); + throw new MqttsnException(e); + } + } + } + + @Override + protected void afterHandle(IMqttsnContext context, IMqttsnMessage messageIn, IMqttsnMessage messageOut) throws MqttsnException { + + super.afterHandle(context, messageIn, messageOut); + + try { + IMqttsnSessionState sessionState = getSessionState(context); + if(sessionState != null){ + registry.getGatewaySessionService().updateLastSeen(sessionState); + } + } catch(MqttsnInvalidSessionStateException e){ + //-- a disconnect will mean theres no session to update + } + } + + @Override + protected void afterResponse(IMqttsnContext context, IMqttsnMessage messageIn, IMqttsnMessage messageOut) throws MqttsnException { + + try { + IMqttsnSessionState sessionState = getSessionState(context); + if(sessionState != null){ + //active session means we can try and see if there is anything to flush here if its a terminal message + if(messageIn != null && isTerminalMessage(messageIn) && !messageIn.isErrorMessage() || + messageOut != null && isTerminalMessage(messageOut) && !messageOut.isErrorMessage() ){ + if(MqttsnUtils.in(sessionState.getClientState(), + MqttsnClientState.CONNECTED, MqttsnClientState.AWAKE)) { + registry.getMessageStateService().scheduleFlush(context); + } + } + } + } catch(MqttsnInvalidSessionStateException e){ + //-- a disconnect will mean theres no session to update + } + } + + @Override + protected IMqttsnMessage handleConnect(IMqttsnContext context, IMqttsnMessage connect) throws MqttsnException, MqttsnCodecException { + + MqttsnConnect connectMessage = (MqttsnConnect) connect ; + if(registry.getPermissionService() != null){ + if(!registry.getPermissionService().allowConnect(context, connectMessage.getClientId())){ + logger.log(Level.WARNING, String.format("permission service rejected client [%s]", connectMessage.getClientId())); + return registry.getMessageFactory().createConnack(MqttsnConstants.RETURN_CODE_SERVER_UNAVAILABLE); + } + } + + IMqttsnSessionState state = getSessionState(context, true); + ConnectResult result = registry.getGatewaySessionService().connect(state, connectMessage.getClientId(), + connectMessage.getDuration(), connectMessage.isCleanSession()); + + processSessionResult(result); + if(result.isError()){ + return registry.getMessageFactory().createConnack(result.getReturnCode()); + } + else { + if(connectMessage.isWill()){ + return registry.getMessageFactory().createWillTopicReq(); + } else { + return registry.getMessageFactory().createConnack(result.getReturnCode()); + } + } + } + + @Override + protected IMqttsnMessage handleDisconnect(IMqttsnContext context, IMqttsnMessage initialDisconnect, IMqttsnMessage receivedDisconnect) throws MqttsnException, MqttsnCodecException, MqttsnInvalidSessionStateException { + + MqttsnDisconnect d = (MqttsnDisconnect) receivedDisconnect; + if(!MqttsnUtils.validUInt16(d.getDuration())){ + logger.log(Level.WARNING, String.format("invalid sleep duration specified, reject client [%s]", d.getDuration())); + return super.handleDisconnect(context, initialDisconnect, receivedDisconnect); + } else { + IMqttsnSessionState state = getSessionState(context, false); + //was disconnected already? + + boolean needsResponse = initialDisconnect != null || + (state != null && !MqttsnUtils.in(state.getClientState(), MqttsnClientState.DISCONNECTED)); + if(state != null && !MqttsnUtils.in(state.getClientState(), MqttsnClientState.DISCONNECTED)){ + registry.getGatewaySessionService().disconnect(state, d.getDuration()); + } + return needsResponse ? + super.handleDisconnect(context, initialDisconnect, receivedDisconnect) : null; + } + } + + @Override + protected IMqttsnMessage handlePingreq(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException, MqttsnCodecException, MqttsnInvalidSessionStateException { + + IMqttsnSessionState state = getSessionState(context); + MqttsnPingreq ping = (MqttsnPingreq) message; + if(ping.getClientId() != null){ + //-- ensure the clientid matches the context + if(!ping.getClientId().trim().equals(context.getId())){ + logger.log(Level.WARNING, "ping-req contained clientId that did not match that from context"); + return super.handlePingreq(context, message); + } + } + + if(MqttsnUtils.in(state.getClientState(), MqttsnClientState.ASLEEP, MqttsnClientState.AWAKE)){ + //-- only wake the client if there is messages outstanding + if(registry.getMessageQueue().size(context) > 0){ + if(state.getClientState() == MqttsnClientState.ASLEEP){ + //-- this is the waking ping.. all is ok + registry.getGatewaySessionService().wake(state); + registry.getMessageStateService().scheduleFlush(context); + + } else if(state.getClientState() == MqttsnClientState.AWAKE){ + //-- this is the client issuing multiple pings when it should be waiting on the messages.. humph + logger.log(Level.INFO, "multiple pings are being sent, clear up and try again the client is getting confused.."); + registry.getMessageStateService().clearInflight(context); + registry.getMessageStateService().scheduleFlush(context); + } + + return null; + } else { + return super.handlePingreq(context, message); + } + } else { + registry.getGatewaySessionService().ping(state); + return super.handlePingreq(context, message); + } + } + + @Override + protected IMqttsnMessage handleSubscribe(IMqttsnContext context, IMqttsnMessage message) + throws MqttsnException, MqttsnCodecException, MqttsnInvalidSessionStateException { + + MqttsnSubscribe subscribe = (MqttsnSubscribe) message; + if(!MqttsnUtils.validTopicScheme(subscribe.getTopicType(), subscribe.getTopicData(), true)){ + logger.log(Level.WARNING, String.format("supplied topic did not appear to be valid, return INVALID TOPIC ID typeId [%s] topicData [%s]", subscribe.getTopicType(), + MqttsnWireUtils.toBinary(subscribe.getTopicData()))); + return registry.getMessageFactory().createSuback(0, 0, MqttsnConstants.RETURN_CODE_INVALID_TOPIC_ID); + } + + IMqttsnSessionState state = getSessionState(context); + TopicInfo info = registry.getTopicRegistry().normalize((byte) subscribe.getTopicType(), subscribe.getTopicData(), + subscribe.getTopicType() == 0); + + + SubscribeResult result = registry.getGatewaySessionService().subscribe(state, info, subscribe.getQoS()); + logger.log(Level.INFO, "subscribe message yeilded info " + info + " and result " + result); + processSessionResult(result); + if(result.isError()){ + return registry.getMessageFactory().createSuback(0, 0, result.getReturnCode()); + } else { + //-- this is a flaw in the current spec, you should be able to send back the topicIdType in the response + IMqttsnMessage suback = registry.getMessageFactory().createSuback(result.getGrantedQoS(), result.getTopicInfo().getTopicId(), result.getReturnCode()); + return suback; + } + } + + @Override + protected IMqttsnMessage handleUnsubscribe(IMqttsnContext context, IMqttsnMessage message) throws MqttsnException, MqttsnCodecException, MqttsnInvalidSessionStateException { + + MqttsnUnsubscribe unsubscribe = (MqttsnUnsubscribe) message; + + if(!MqttsnUtils.validTopicScheme(unsubscribe.getTopicType(), unsubscribe.getTopicData(), true)){ + logger.log(Level.WARNING, String.format("supplied topic did not appear to be valid, return INVALID TOPIC ID typeId [%s] topicData [%s]", unsubscribe.getTopicType(), + MqttsnWireUtils.toBinary(unsubscribe.getTopicData()))); + return registry.getMessageFactory().createUnsuback(); + } + + IMqttsnSessionState state = getSessionState(context); + TopicInfo info = registry.getTopicRegistry().normalize((byte) unsubscribe.getTopicType(), unsubscribe.getTopicData(), true); + UnsubscribeResult result = registry.getGatewaySessionService().unsubscribe(state, info); + processSessionResult(result); + return registry.getMessageFactory().createUnsuback(); + } + + @Override + protected IMqttsnMessage handleRegister(IMqttsnContext context, IMqttsnMessage message) + throws MqttsnException, MqttsnCodecException, MqttsnInvalidSessionStateException { + + MqttsnRegister register = (MqttsnRegister) message; + + if(!MqttsnUtils.validTopicName(register.getTopicName())){ + logger.log(Level.WARNING, + String.format("invalid topic [%s] received during register, reply with error code", register.getTopicName())); + return registry.getMessageFactory().createRegack(0, MqttsnConstants.RETURN_CODE_INVALID_TOPIC_ID); + } else { + IMqttsnSessionState state = getSessionState(context); + RegisterResult result = registry.getGatewaySessionService().register(state, register.getTopicName()); + processSessionResult(result); + return registry.getMessageFactory().createRegack(result.getTopicInfo().getTopicId(), MqttsnConstants.RETURN_CODE_ACCEPTED); + } + } + + @Override + protected IMqttsnMessage handlePublish(IMqttsnContext context, IMqttsnMessage message) + throws MqttsnException, MqttsnCodecException, MqttsnInvalidSessionStateException { + + MqttsnPublish publish = (MqttsnPublish) message; + IMqttsnSessionState state = null; + try { + state = getSessionState(context); + } catch(MqttsnInvalidSessionStateException e){ + //-- connectionless publish (m1) + if(publish.getQoS() != MqttsnConstants.QoSM1) + throw e; + } + + return super.handlePublish(context, message); + } + + protected void processSessionResult(Result result){ + if(result.getStatus() == Result.STATUS.ERROR){ + logger.log(Level.WARNING, String.format("error detected by session service [%s]", result)); + } + } +} \ No newline at end of file diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/gateway/MqttsnGatewayPermissionService.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/gateway/MqttsnGatewayPermissionService.java new file mode 100644 index 00000000..efed5ad3 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/gateway/MqttsnGatewayPermissionService.java @@ -0,0 +1,39 @@ +package org.slj.mqtt.sn.gateway.impl.gateway; + +import org.slj.mqtt.sn.gateway.spi.gateway.MqttsnGatewayOptions; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.spi.IMqttsnPermissionService; +import org.slj.mqtt.sn.spi.IMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.spi.MqttsnException; +import org.slj.mqtt.sn.spi.MqttsnService; + +import java.util.Set; + +public class MqttsnGatewayPermissionService + extends MqttsnService implements IMqttsnPermissionService { + + @Override + public boolean allowConnect(IMqttsnContext context, String clientId) throws MqttsnException { + Set allowedClientId = ((MqttsnGatewayOptions)registry.getOptions()).getAllowedClientIds(); + if(allowedClientId != null && !allowedClientId.isEmpty()){ + return allowedClientId.contains(MqttsnGatewayOptions.DEFAULT_CLIENT_ALLOWED_ALL) + || allowedClientId.contains(clientId); + } + return false; + } + + @Override + public boolean allowedToSubscribe(IMqttsnContext context, String topicPath) throws MqttsnException { + return true; + } + + @Override + public int allowedMaximumQoS(IMqttsnContext context, String topicPath) throws MqttsnException { + return 2; + } + + @Override + public boolean allowedToPublish(IMqttsnContext context, String topicPath, int size, int QoS) throws MqttsnException { + return true; + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/gateway/MqttsnGatewayQueueProcessorStateService.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/gateway/MqttsnGatewayQueueProcessorStateService.java new file mode 100644 index 00000000..75b86c20 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/gateway/MqttsnGatewayQueueProcessorStateService.java @@ -0,0 +1,40 @@ +package org.slj.mqtt.sn.gateway.impl.gateway; + +import org.slj.mqtt.sn.gateway.spi.gateway.IMqttsnGatewayRuntimeRegistry; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.IMqttsnSessionState; +import org.slj.mqtt.sn.model.MqttsnClientState; +import org.slj.mqtt.sn.spi.IMqttsnMessage; +import org.slj.mqtt.sn.spi.IMqttsnQueueProcessorStateService; +import org.slj.mqtt.sn.spi.MqttsnException; +import org.slj.mqtt.sn.spi.MqttsnService; +import org.slj.mqtt.sn.utils.MqttsnUtils; + +import java.util.logging.Level; + +public class MqttsnGatewayQueueProcessorStateService extends MqttsnService + implements IMqttsnQueueProcessorStateService { + + @Override + public boolean canReceive(IMqttsnContext context) throws MqttsnException { + IMqttsnSessionState state = getRegistry().getGatewaySessionService().getSessionState(context, false); + return state != null && MqttsnUtils.in(state.getClientState() , MqttsnClientState.CONNECTED, MqttsnClientState.AWAKE); + } + + @Override + public void queueEmpty(IMqttsnContext context) throws MqttsnException { + + IMqttsnSessionState state = getRegistry().getGatewaySessionService().getSessionState(context, false); + logger.log(Level.INFO, String.format("notified that the queue is empty, post process state is - [%s]", state)); + if(state != null){ + if(MqttsnUtils.in(state.getClientState() , MqttsnClientState.AWAKE)){ + logger.log(Level.INFO, String.format("notified that the queue is empty, putting device back to sleep and sending ping-resp - [%s]", context)); + //-- need to transition the device back to sleep + getRegistry().getGatewaySessionService().disconnect(state, state.getKeepAlive()); + //-- need to send the closing ping-resp + IMqttsnMessage pingResp = getRegistry().getMessageFactory().createPingresp(); + getRegistry().getTransport().writeToTransport(getRegistry().getNetworkRegistry().getContext(context), pingResp); + } + } + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/gateway/MqttsnGatewaySessionService.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/gateway/MqttsnGatewaySessionService.java new file mode 100644 index 00000000..f3c33bad --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/impl/gateway/MqttsnGatewaySessionService.java @@ -0,0 +1,365 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.impl.gateway; + +import org.slj.mqtt.sn.MqttsnConstants; +import org.slj.mqtt.sn.gateway.spi.*; +import org.slj.mqtt.sn.gateway.spi.gateway.IMqttsnGatewayRuntimeRegistry; +import org.slj.mqtt.sn.gateway.spi.gateway.IMqttsnGatewaySessionRegistryService; +import org.slj.mqtt.sn.gateway.spi.gateway.MqttsnGatewayOptions; +import org.slj.mqtt.sn.impl.AbstractMqttsnBackoffThreadService; +import org.slj.mqtt.sn.model.*; +import org.slj.mqtt.sn.spi.MqttsnException; +import org.slj.mqtt.sn.utils.TopicPath; + +import java.util.*; +import java.util.logging.Level; + +public class MqttsnGatewaySessionService extends AbstractMqttsnBackoffThreadService + implements IMqttsnGatewaySessionRegistryService { + + protected Map sessionLookup; + + @Override + public void start(IMqttsnGatewayRuntimeRegistry runtime) throws MqttsnException { + super.start(runtime); + sessionLookup = Collections.synchronizedMap(new HashMap()); + } + + @Override + protected long doWork() { + synchronized (sessionLookup){ + Iterator itr = sessionLookup.keySet().iterator(); + while(itr.hasNext()){ + IMqttsnContext context = itr.next(); + IMqttsnSessionState state = sessionLookup.get(context); + deamon_validateKeepAlive(state); + } + } + return 10000; + } + + protected void deamon_validateKeepAlive(IMqttsnSessionState state){ + if(state.getClientState() == MqttsnClientState.CONNECTED || + state.getClientState() == MqttsnClientState.ASLEEP){ + long time = System.currentTimeMillis(); + if(state != null && state.getKeepAlive() > 0){ + long lastSeen = state.getLastSeen().getTime(); + if(lastSeen + (state.getKeepAlive() * 1000) < time){ + logger.log(Level.WARNING, String.format("keep-alive deamon detected stale session for [%s], disconnecting", state.getContext())); + state.setClientState(MqttsnClientState.DISCONNECTED); + } + } + } + } + + @Override + public IMqttsnSessionState getSessionState(IMqttsnContext context, boolean createIfNotExists) { + IMqttsnSessionState state = sessionLookup.get(context); + if(state == null && createIfNotExists){ + synchronized (this){ + if((state = sessionLookup.get(context)) == null){ + state = new MqttsnSessionState(context, MqttsnClientState.PENDING); + sessionLookup.put(context, state); + } + } + } + return state; + } + + @Override + public ConnectResult connect(IMqttsnSessionState state, String clientId, int keepAlive, boolean cleanSession) throws MqttsnException { + ConnectResult result = null; + result = checkSessionSize(clientId); + if(result == null){ + synchronized (state.getContext()){ + try { + result = registry.getBrokerService().connect(state.getContext(), state.getContext().getId(), cleanSession, keepAlive); + } finally { + if(!result.isError()){ + //clear down all prior session state + cleanSession(state.getContext(), cleanSession); + state.setKeepAlive(keepAlive); + state.setClientState(MqttsnClientState.CONNECTED); + } else { + //-- connect was not successful ensure we + //-- do not hold a reference to any session + clear(state.getContext()); + } + } + } + } + + logger.log(Level.INFO, String.format("handled connection request for [%s] with cleanSession [%s] -> [%s], [%s]", state.getContext(), cleanSession, result.getStatus(), result.getMessage())); + return result; + } + + @Override + public void disconnect(IMqttsnSessionState state, int duration) throws MqttsnException { + DisconnectResult result = null; + synchronized (state.getContext()){ + result = registry.getBrokerService().disconnect(state.getContext(), duration); + if(!result.isError()){ + if(duration > 0){ + logger.log(Level.INFO, String.format("[%s] setting client state asleep for [%s]", state.getContext(), duration)); + state.setKeepAlive(duration); + state.setClientState(MqttsnClientState.ASLEEP); + registry.getTopicRegistry().clear(state.getContext(), + registry.getOptions().isSleepClearsRegistrations()); + } else { + logger.log(Level.INFO, String.format("[%s] disconnecting client", state.getContext())); + sessionLookup.remove(state.getContext()); + } + } + } + } + + @Override + public SubscribeResult subscribe(IMqttsnSessionState state, TopicInfo info, int QoS) throws MqttsnException { + + IMqttsnContext context = state.getContext(); + synchronized (context){ + String topicPath = null; + if(info.getType() == MqttsnConstants.TOPIC_TYPE.PREDEFINED){ + topicPath = registry.getTopicRegistry().lookupPredefined(context, info.getTopicId()); + info = new TopicInfo(MqttsnConstants.TOPIC_TYPE.PREDEFINED, info.getTopicId()); + } else { + topicPath = info.getTopicPath(); + if(!TopicPath.isValidSubscription(topicPath, registry.getOptions().getMaxTopicLength())){ + return new SubscribeResult(Result.STATUS.ERROR, MqttsnConstants.RETURN_CODE_INVALID_TOPIC_ID, + "invalid topic format"); + } + if(!TopicPath.isWild(topicPath)){ + TopicInfo lookupInfo = registry.getTopicRegistry().lookup(state.getContext(), topicPath); + if(lookupInfo == null || info.getType() == MqttsnConstants.TOPIC_TYPE.NORMAL){ + info = registry.getTopicRegistry().register(state.getContext(), topicPath); + } + } else { + info = TopicInfo.WILD; + } + } + + if(topicPath == null){ + + //-- topic could not be found to lookup + return new SubscribeResult(Result.STATUS.ERROR, MqttsnConstants.RETURN_CODE_INVALID_TOPIC_ID, + "no topic found by specification"); + + } else { + if(registry.getPermissionService() != null){ + if(!registry.getPermissionService().allowedToSubscribe(context, topicPath)){ + return new SubscribeResult(Result.STATUS.ERROR, MqttsnConstants.RETURN_CODE_REJECTED_CONGESTION, + "permission service denied subscription"); + } + QoS = Math.min(registry.getPermissionService().allowedMaximumQoS(context, topicPath), QoS); + } + + if(registry.getSubscriptionRegistry().subscribe(state.getContext(), topicPath, QoS)){ + SubscribeResult result = registry.getBrokerService().subscribe(context, topicPath, QoS); + result.setTopicInfo(info); + return result; + } else { + SubscribeResult result = new SubscribeResult(Result.STATUS.NOOP); + result.setTopicInfo(info); + result.setGrantedQoS(QoS); + return result; + } + } + } + } + + @Override + public UnsubscribeResult unsubscribe(IMqttsnSessionState state, TopicInfo info) throws MqttsnException { + + IMqttsnContext context = state.getContext(); + synchronized (context){ + String topicPath = null; + if(info.getType() == MqttsnConstants.TOPIC_TYPE.PREDEFINED){ + topicPath = registry.getTopicRegistry().lookupPredefined(context, info.getTopicId()); + info = new TopicInfo(MqttsnConstants.TOPIC_TYPE.PREDEFINED, info.getTopicId()); + } else { + topicPath = info.getTopicPath(); + if(!TopicPath.isValidSubscription(topicPath, registry.getOptions().getMaxTopicLength())){ + return new UnsubscribeResult(Result.STATUS.ERROR, MqttsnConstants.RETURN_CODE_INVALID_TOPIC_ID, + "invalid topic format"); + } + if(!TopicPath.isWild(topicPath)){ + TopicInfo lookupInfo = registry.getTopicRegistry().lookup(state.getContext(), topicPath); + if(lookupInfo == null || info.getType() == MqttsnConstants.TOPIC_TYPE.NORMAL){ + info = registry.getTopicRegistry().register(state.getContext(), topicPath); + } + } else { + info = TopicInfo.WILD; + } + } + + if(topicPath == null){ + //-- topic could not be found to lookup + return new UnsubscribeResult(Result.STATUS.ERROR, MqttsnConstants.RETURN_CODE_INVALID_TOPIC_ID, + "no topic found by specification"); + } else { + if(registry.getSubscriptionRegistry().unsubscribe(context, topicPath)){ + UnsubscribeResult result = registry.getBrokerService().unsubscribe(context, topicPath); + return result; + } else { + return new UnsubscribeResult(Result.STATUS.NOOP); + } + } + } + } + + @Override + public RegisterResult register(IMqttsnSessionState state, String topicPath) throws MqttsnException { + + if(!TopicPath.isValidSubscription(topicPath, registry.getOptions().getMaxTopicLength())){ + return new RegisterResult(Result.STATUS.ERROR, MqttsnConstants.RETURN_CODE_INVALID_TOPIC_ID, "invalid topic format"); + } + synchronized (state.getContext()){ + TopicInfo info; + if(!TopicPath.isWild(topicPath)){ + info = registry.getTopicRegistry().lookup(state.getContext(), topicPath); + if(info == null){ + info = registry.getTopicRegistry().register(state.getContext(), topicPath); + } + } else { + info = TopicInfo.WILD; + } + return new RegisterResult(topicPath, info); + } + } + + @Override + public void ping(IMqttsnSessionState state) { + } + + @Override + public void wake(IMqttsnSessionState state) { + state.setClientState(MqttsnClientState.AWAKE); + } + + @Override + public void updateLastSeen(IMqttsnSessionState state) { + state.setLastSeen(new Date()); + } + + public void cleanSession(IMqttsnContext context, boolean deepClean) throws MqttsnException { + + //clear down all prior session state + synchronized (context){ + if(deepClean){ + //-- the queued messages + registry.getMessageQueue().clear(context); + + //-- the subscriptions + registry.getSubscriptionRegistry().clear(context); + } + + //-- inflight messages & protocol messages + registry.getMessageStateService().clear(context); + + //-- topic registrations + registry.getTopicRegistry().clear(context); + } + } + + public void clearAll() { + sessionLookup.clear(); + } + + @Override + public void clear(IMqttsnContext context) { + logger.log(Level.INFO, String.format(String.format("removing context from active/sleepings sessions [%s]", context))); + sessionLookup.remove(context); + } + + protected ConnectResult checkSessionSize(String clientId){ + + int maxConnectedClients = ((MqttsnGatewayOptions) registry.getOptions()).getMaxConnectedClients(); + if(sessionLookup.size() >= maxConnectedClients){ + return new ConnectResult(Result.STATUS.ERROR, MqttsnConstants.RETURN_CODE_REJECTED_CONGESTION, "gateway has reached capacity"); + } + return null; + } + + + + @Override + public void receiveToSessions(String topicPath, byte[] payload, int QoS) throws MqttsnException { + //-- expand the message onto the gateway connected device queues + List recipients = registry.getSubscriptionRegistry().matches(topicPath); + logger.log(Level.INFO, String.format("receiving broker side message into [%s] sessions", recipients.size())); + + //if we only have 1 reciever remove message after read + UUID messageId = recipients.size() > 1 ? + registry.getMessageRegistry().add(payload, calculateExpiry()) : + registry.getMessageRegistry().add(payload, true) ; + + for (IMqttsnContext client : recipients){ + int grantedQos = registry.getSubscriptionRegistry().getQos(client, topicPath); + int q = Math.min(grantedQos,QoS); + try { + registry.getMessageQueue().offer(client, new QueuedPublishMessage( + messageId, topicPath, q)); + } catch(MqttsnQueueAcceptException e){ + throw new MqttsnException(e); + } + } + } + + protected Date calculateExpiry(){ + Calendar cal = Calendar.getInstance(); + cal.setTime(new Date()); + cal.add(Calendar.YEAR, 1); + return cal.getTime(); + } + + @Override + protected String getDaemonName() { + return "gateway-session"; + } + + @Override + public Optional lookupClientIdSession(String clientId){ + synchronized (sessionLookup){ + Iterator itr = sessionLookup.keySet().iterator(); + while(itr.hasNext()){ + IMqttsnContext c = itr.next(); + if(c != null && c.getId().equals(clientId)) + return Optional.of(c); + } + } + return Optional.empty(); + } + + @Override + public Iterator iterator() { + Set copy = null; + synchronized (sessionLookup){ + Set s = sessionLookup.keySet(); + copy = new HashSet(s); + } + return copy.iterator(); + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/ConnectResult.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/ConnectResult.java new file mode 100644 index 00000000..09ce3ce9 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/ConnectResult.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi; + +public class ConnectResult extends Result { + + public ConnectResult(STATUS status, String message) { + super(status); + setMessage(message); + } + + public ConnectResult(STATUS status, int returnCode, String message) { + super(status); + setMessage(message); + setReturnCode(returnCode); + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/DisconnectResult.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/DisconnectResult.java new file mode 100644 index 00000000..2b5f849b --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/DisconnectResult.java @@ -0,0 +1,15 @@ +package org.slj.mqtt.sn.gateway.spi; + +public class DisconnectResult extends Result { + + public DisconnectResult(STATUS status, String message) { + super(status); + setMessage(message); + } + + public DisconnectResult(STATUS status, int returnCode, String message) { + super(status); + setMessage(message); + setReturnCode(returnCode); + } +} \ No newline at end of file diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/MqttsnInvalidSessionStateException.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/MqttsnInvalidSessionStateException.java new file mode 100644 index 00000000..020f9da0 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/MqttsnInvalidSessionStateException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi; + +import org.slj.mqtt.sn.spi.MqttsnRuntimeException; + +public class MqttsnInvalidSessionStateException extends MqttsnRuntimeException { + + public MqttsnInvalidSessionStateException() { + } + + public MqttsnInvalidSessionStateException(String message) { + super(message); + } + + public MqttsnInvalidSessionStateException(String message, Throwable cause) { + super(message, cause); + } + + public MqttsnInvalidSessionStateException(Throwable cause) { + super(cause); + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/PublishResult.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/PublishResult.java new file mode 100644 index 00000000..0a2e6b7e --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/PublishResult.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi; + +public class PublishResult extends Result { + + public PublishResult(STATUS status, String message) { + super(status); + setMessage(message); + } + + public PublishResult(STATUS status, int returnCode, String message) { + super(status); + setMessage(message); + setReturnCode(returnCode); + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/RegisterResult.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/RegisterResult.java new file mode 100644 index 00000000..4bd1a404 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/RegisterResult.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi; + +import org.slj.mqtt.sn.model.TopicInfo; + +public class RegisterResult extends Result { + + private TopicInfo topicInfo; + private String topicPath; + + public RegisterResult(STATUS status) { + super(status); + } + + public RegisterResult(String topicPath, TopicInfo info) { + super(STATUS.SUCCESS); + this.topicInfo = info; + this.topicPath = topicPath; + } + + public RegisterResult(STATUS status, int returnCode, String message) { + super(status); + setMessage(message); + setReturnCode(returnCode); + } + + public String getTopicPath() { + return topicPath; + } + + public TopicInfo getTopicInfo() { + return topicInfo; + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/Result.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/Result.java new file mode 100644 index 00000000..d9c60705 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/Result.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi; + +import org.slj.mqtt.sn.MqttsnConstants; + +public class Result { + + protected STATUS status; + protected String message; + protected int returnCode; + + public enum STATUS { + SUCCESS, ERROR, NOOP + } + + public Result(STATUS status) { + this.status = status; + if(status == STATUS.SUCCESS){ + returnCode = MqttsnConstants.RETURN_CODE_ACCEPTED; + } else if (status == STATUS.ERROR){ + returnCode = MqttsnConstants.RETURN_CODE_SERVER_UNAVAILABLE; + } + } + + public Result(STATUS status, int returnCode) { + this.status = status; + this.returnCode = returnCode; + } + + public Result(STATUS status, int returnCode, String message) { + this.status = status; + this.returnCode = returnCode; + this.message = message; + } + + public STATUS getStatus() { + return status; + } + + public void setStatus(STATUS status) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public int getReturnCode() { + return returnCode; + } + + public void setReturnCode(int returnCode) { + this.returnCode = returnCode; + } + + public boolean isError(){ + return STATUS.ERROR == status; + } + + @Override + public String toString() { + return "Result{" + + "status=" + status + + ", message='" + message + '\'' + + ", returnCode=" + returnCode + + '}'; + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/SubscribeResult.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/SubscribeResult.java new file mode 100644 index 00000000..3335d225 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/SubscribeResult.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi; + +import org.slj.mqtt.sn.model.TopicInfo; + +public class SubscribeResult extends Result { + + private TopicInfo topicInfo; + private int grantedQoS; + + public SubscribeResult(STATUS status, int returnCode, String message) { + super(status); + setMessage(message); + setReturnCode(returnCode); + } + + public SubscribeResult(STATUS status) { + super(status); + } + + public SubscribeResult(TopicInfo info, int grantedQoS) { + super(STATUS.SUCCESS); + this.topicInfo = info; + this.grantedQoS = grantedQoS; + } + + public TopicInfo getTopicInfo() { + return topicInfo; + } + + public void setTopicInfo(TopicInfo topicInfo) { + this.topicInfo = topicInfo; + } + + public int getGrantedQoS() { + return grantedQoS; + } + + @Override + public String toString() { + return "SubscribeResult{" + + "status=" + status + + ", message='" + message + '\'' + + ", returnCode=" + returnCode + + ", topicInfo=" + topicInfo + + ", grantedQoS=" + grantedQoS + + '}'; + } + + public void setGrantedQoS(int grantedQoS) { + this.grantedQoS = grantedQoS; + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/UnsubscribeResult.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/UnsubscribeResult.java new file mode 100644 index 00000000..a755b0f6 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/UnsubscribeResult.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi; + +public class UnsubscribeResult extends Result { + + public UnsubscribeResult(STATUS status){ + super(status); + } + + public UnsubscribeResult(STATUS status, String message) { + super(status); + setMessage(message); + } + + public UnsubscribeResult(STATUS status, int returnCode, String message) { + super(status); + setMessage(message); + setReturnCode(returnCode); + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/broker/IMqttsnBrokerConnection.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/broker/IMqttsnBrokerConnection.java new file mode 100644 index 00000000..696a5738 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/broker/IMqttsnBrokerConnection.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi.broker; + +import org.slj.mqtt.sn.model.IMqttsnContext; + +import java.io.Closeable; + +public interface IMqttsnBrokerConnection extends Closeable { + + boolean isConnected() throws MqttsnBrokerException; + + void close(); + + boolean disconnect(IMqttsnContext context, int keepAlive) throws MqttsnBrokerException; + + boolean connect(IMqttsnContext context, boolean cleanSession, int keepAlive) throws MqttsnBrokerException; + + boolean subscribe(IMqttsnContext context, String topicPath, int QoS) throws MqttsnBrokerException; + + boolean unsubscribe(IMqttsnContext context, String topicPath) throws MqttsnBrokerException; + + boolean publish(IMqttsnContext context, String topicPath, int QoS, boolean retain, byte[] data) throws MqttsnBrokerException; +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/broker/IMqttsnBrokerConnectionFactory.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/broker/IMqttsnBrokerConnectionFactory.java new file mode 100644 index 00000000..1246313e --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/broker/IMqttsnBrokerConnectionFactory.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi.broker; + +public interface IMqttsnBrokerConnectionFactory { + + T createConnection(MqttsnBrokerOptions options, String clientId) throws MqttsnBrokerException; +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/broker/IMqttsnBrokerService.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/broker/IMqttsnBrokerService.java new file mode 100644 index 00000000..d28e61e2 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/broker/IMqttsnBrokerService.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi.broker; + +import org.slj.mqtt.sn.gateway.spi.*; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.spi.MqttsnException; + +public interface IMqttsnBrokerService { + + boolean isConnected(IMqttsnContext context) throws MqttsnBrokerException; + + ConnectResult connect(IMqttsnContext context, String clientId, boolean cleanSession, int keepAlive) throws MqttsnBrokerException; + + DisconnectResult disconnect(IMqttsnContext context, int keepAlive) throws MqttsnBrokerException ; + + PublishResult publish(IMqttsnContext context, String topicPath, int QoS, byte[] payload) throws MqttsnBrokerException; + + SubscribeResult subscribe(IMqttsnContext context, String topicPath, int QoS) throws MqttsnBrokerException; + + UnsubscribeResult unsubscribe(IMqttsnContext context, String topicPath) throws MqttsnBrokerException; + + void receive(String topicPath, byte[] payload, int QoS) throws MqttsnException; + +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/broker/MqttsnBrokerException.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/broker/MqttsnBrokerException.java new file mode 100644 index 00000000..7d77d516 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/broker/MqttsnBrokerException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi.broker; + +import org.slj.mqtt.sn.spi.MqttsnException; + +public class MqttsnBrokerException extends MqttsnException { + + public MqttsnBrokerException() { + } + + public MqttsnBrokerException(String message) { + super(message); + } + + public MqttsnBrokerException(String message, Throwable cause) { + super(message, cause); + } + + public MqttsnBrokerException(Throwable cause) { + super(cause); + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/broker/MqttsnBrokerOptions.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/broker/MqttsnBrokerOptions.java new file mode 100644 index 00000000..1886786e --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/broker/MqttsnBrokerOptions.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi.broker; + +import java.util.Objects; + +public final class MqttsnBrokerOptions { + + public static final boolean DEFAULT_CONNECT_ON_STARTUP = true; + public static final boolean DEFAULT_MANAGED_CONNECTIONS = true; + public static final int DEFAULT_KEEPALIVE = 30 * 10; + public static final int DEFAULT_CONNECTION_TIMEOUT = 30; + public static final int DEFAULT_MQTT_PORT = 1883; + public static final String DEFAULT_MQTT_PROTOCOL = "tcp"; + + private boolean connectOnStartup = DEFAULT_CONNECT_ON_STARTUP; + private boolean managedConnections = DEFAULT_MANAGED_CONNECTIONS; + private String username; + private String password; + private int keepAlive = DEFAULT_KEEPALIVE; + private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT; + private String host; + private int port = DEFAULT_MQTT_PORT; + private String protocol = DEFAULT_MQTT_PROTOCOL; + + public MqttsnBrokerOptions(){ + + } + + public MqttsnBrokerOptions withConnectOnStartup(boolean connectOnStartup){ + this.connectOnStartup = connectOnStartup; + return this; + } + + public MqttsnBrokerOptions withManagedConnections(boolean managedConnections){ + this.managedConnections = managedConnections; + return this; + } + + public MqttsnBrokerOptions withProtocol(String protocol){ + this.protocol = protocol; + return this; + } + + public MqttsnBrokerOptions withUsername(String username){ + this.username = username; + return this; + } + + public MqttsnBrokerOptions withPassword(String password){ + this.password = password; + return this; + } + + public MqttsnBrokerOptions withKeepAlive(int keepAlive){ + this.keepAlive = keepAlive; + return this; + } + + public MqttsnBrokerOptions withConnectionTimeout(int connectionTimeout){ + this.connectionTimeout = connectionTimeout; + return this; + } + + public MqttsnBrokerOptions withHost(String host){ + this.host = host; + return this; + } + + public MqttsnBrokerOptions withPort(int port){ + this.port = port; + return this; + } + + public boolean getManagedConnections() { + return managedConnections; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public int getKeepAlive() { + return keepAlive; + } + + public int getConnectionTimeout() { + return connectionTimeout; + } + + public String getHost() { + return host; + } + + public boolean getConnectOnStartup() { + return connectOnStartup; + } + + public int getPort() { + return port; + } + + public String getProtocol() { + return protocol; + } + + public boolean validConnectionDetails(){ + return !nonEmpty(protocol) && !nonEmpty(host) && port > 0 && !nonEmpty(username); + } + + static boolean nonEmpty(String val){ + return !Objects.isNull(val) && "".equals(val.trim()); + } +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/gateway/IMqttsnGatewayAdvertiseService.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/gateway/IMqttsnGatewayAdvertiseService.java new file mode 100644 index 00000000..e404031e --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/gateway/IMqttsnGatewayAdvertiseService.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi.gateway; + +import org.slj.mqtt.sn.spi.IMqttsnRuntimeRegistry; +import org.slj.mqtt.sn.spi.IMqttsnService; + +public interface IMqttsnGatewayAdvertiseService extends IMqttsnService { + + + +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/gateway/IMqttsnGatewayRuntimeRegistry.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/gateway/IMqttsnGatewayRuntimeRegistry.java new file mode 100644 index 00000000..1b9fef99 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/gateway/IMqttsnGatewayRuntimeRegistry.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi.gateway; + +import org.slj.mqtt.sn.gateway.spi.broker.IMqttsnBrokerConnectionFactory; +import org.slj.mqtt.sn.gateway.spi.broker.IMqttsnBrokerService; +import org.slj.mqtt.sn.spi.IMqttsnRuntimeRegistry; + +public interface IMqttsnGatewayRuntimeRegistry extends IMqttsnRuntimeRegistry { + + IMqttsnBrokerService getBrokerService(); + + IMqttsnBrokerConnectionFactory getBrokerConnectionFactory(); + + IMqttsnGatewaySessionRegistryService getGatewaySessionService(); + + IMqttsnGatewayAdvertiseService getGatewayAdvertiseService(); + +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/gateway/IMqttsnGatewaySessionRegistryService.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/gateway/IMqttsnGatewaySessionRegistryService.java new file mode 100644 index 00000000..9c19b63c --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/gateway/IMqttsnGatewaySessionRegistryService.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi.gateway; + +import org.slj.mqtt.sn.gateway.spi.ConnectResult; +import org.slj.mqtt.sn.gateway.spi.RegisterResult; +import org.slj.mqtt.sn.gateway.spi.SubscribeResult; +import org.slj.mqtt.sn.gateway.spi.UnsubscribeResult; +import org.slj.mqtt.sn.model.IMqttsnContext; +import org.slj.mqtt.sn.model.IMqttsnSessionState; +import org.slj.mqtt.sn.model.TopicInfo; +import org.slj.mqtt.sn.spi.IMqttsnRegistry; +import org.slj.mqtt.sn.spi.MqttsnException; + +import java.util.Iterator; +import java.util.Optional; + +public interface IMqttsnGatewaySessionRegistryService extends IMqttsnRegistry { + + Optional lookupClientIdSession(String clientId) throws MqttsnException; + + IMqttsnSessionState getSessionState(IMqttsnContext context, boolean createIfNotExists) throws MqttsnException; + + ConnectResult connect(IMqttsnSessionState state, String clientId, int keepAlive, boolean cleanSession) throws MqttsnException; + + SubscribeResult subscribe(IMqttsnSessionState state, TopicInfo info, int QoS) throws MqttsnException; + + UnsubscribeResult unsubscribe(IMqttsnSessionState state, TopicInfo info) throws MqttsnException; + + RegisterResult register(IMqttsnSessionState state, String topicPath) throws MqttsnException; + + void wake(IMqttsnSessionState state) throws MqttsnException; + + void ping(IMqttsnSessionState state) throws MqttsnException; + + void updateLastSeen(IMqttsnSessionState state); + + void cleanSession(IMqttsnContext state, boolean deepClean) throws MqttsnException; + + void disconnect(IMqttsnSessionState state, int duration) throws MqttsnException; + + void receiveToSessions(String topicPath, byte[] payload, int QoS) throws MqttsnException ; + + Iterator iterator(); +} diff --git a/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/gateway/MqttsnGatewayOptions.java b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/gateway/MqttsnGatewayOptions.java new file mode 100644 index 00000000..b179e918 --- /dev/null +++ b/mqtt-sn-gateway/src/main/java/org/slj/mqtt/sn/gateway/spi/gateway/MqttsnGatewayOptions.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020 Simon Johnson + * + * Find me on GitHub: + * https://github.com/simon622 + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.slj.mqtt.sn.gateway.spi.gateway; + +import org.slj.mqtt.sn.model.MqttsnOptions; + +import java.util.HashSet; +import java.util.Set; + +public final class MqttsnGatewayOptions extends MqttsnOptions { + + /** + * By default, any clientId ("*") will be allowed to connect to the gateway. + */ + public static final String DEFAULT_CLIENT_ALLOWED_ALL = "*"; + + /** + * The default gatewayId used in advertise / discovery is 1 + */ + public static final int DEFAULT_GATEWAY_ID = 1; + + /** + * A default gateway will allow 100 simultaneous connects to reside on the gateway + */ + public static final int DEFAULT_MAX_CONNECTED_CLIENTS = 100; + + /** + * The default advertise time is 60 seconds + */ + public static final int DEFAULT_GATEWAY_ADVERTISE_TIME = 60; + + private Set allowedClientIds = new HashSet(); + { + allowedClientIds.add(DEFAULT_CLIENT_ALLOWED_ALL); + } + + private int maxConnectedClients = DEFAULT_MAX_CONNECTED_CLIENTS; + private int gatewayAdvertiseTime = DEFAULT_GATEWAY_ADVERTISE_TIME; + private int gatewayId = DEFAULT_GATEWAY_ID; + + public MqttsnGatewayOptions withMaxConnectedClients(int maxConnectedClients){ + this.maxConnectedClients = maxConnectedClients; + return this; + } + + public MqttsnGatewayOptions withGatewayId(int gatewayId){ + this.gatewayId = gatewayId; + return this; + } + + public MqttsnGatewayOptions withGatewayAdvertiseTime(int gatewayAdvertiseTime){ + this.gatewayAdvertiseTime = gatewayAdvertiseTime; + return this; + } + + public int getGatewayAdvertiseTime() { + return gatewayAdvertiseTime; + } + + public int getGatewayId() { + return gatewayId; + } + + public int getMaxConnectedClients() { + return maxConnectedClients; + } + + public Set getAllowedClientIds() { + return allowedClientIds; + } + + public MqttsnGatewayOptions withAllowedClientId(String clientId){ + + //-- if the application specifies custom allow list, remove wildcard + if(allowedClientIds.contains(DEFAULT_CLIENT_ALLOWED_ALL)){ + allowedClientIds.remove(DEFAULT_CLIENT_ALLOWED_ALL); + } + allowedClientIds.add(clientId); + return this; + } +}