diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 8dada3edaf5..00000000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000000..aeea9995886 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,63 @@ +Apache License +Version 2.0, January 2004 + +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: + + 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and + 2. You must cause any modified files to carry prominent notices stating that You changed the files; and + 3. 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 + 4. 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 + +Note: Other license terms may apply to certain, identified software files contained within or distributed with the accompanying software if such terms are included in the directory containing the accompanying software. Such other license terms will then apply in lieu of the terms of the software license above. + +JSON processing code subject to the JSON License from JSON.org: + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 00000000000..979460ec735 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,13 @@ +AWS IoT Device SDK for Java +Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +This product includes software developed by +Amazon Technologies, Inc (http://www.amazon.com/). + +********************** +THIRD PARTY COMPONENTS +********************** +This software includes third party software subject to the following copyrights: +- PKCS#1 and PKCS#8 PEM encoded private key parsing and utility functions from oauth.googlecode.com - Copyright 1998-2010 AOL Inc. + +The licenses for these third party components are included in LICENSE.txt \ No newline at end of file diff --git a/README.md b/README.md index f7af675ae8c..fa1ebaa48a1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,494 @@ -# aws-iot-device-sdk-java -SDK for connecting to AWS IoT from a device. +# AWS IoT Device SDK for Java +The **AWS IoT Device SDK for Java** enables Java developers to access the AWS +IoT Platform through [MQTT or MQTT over the WebSocket protocol][aws-iot-protocol]. +The SDK is built with [AWS IoT device shadow support][aws-iot-thing], providing +access to thing shadows (sometimes referred to as device shadows) using shadow methods, including GET, UPDATE, and DELETE. +It also supports a simplified shadow access model, which allows developers to +exchange data with their shadows by just using getter and setter methods without +having to serialize or deserialize any JSON documents. + +To get started, use the Maven repository or download the [latest JAR file][latest-jar]. + +* [Overview](#overview) +* [Install the SDK](#install-the-sdk) +* [Use the SDK](#use-the-sdk) +* [Sample Applications](#sample-applications) +* [API Documentation](#api-documentation) +* [License](#license) +* [Support](#support) + +## Overview +This document provides instructions for installing and configuring the AWS +IoT device SDK for Java. It also includes some examples that demonstrate the use of different +APIs. + +### MQTT Connection Types +The SDK is built on top of the [Paho MQTT Java client library][paho-mqtt-java-download]. +Developers can choose from two types of connections to connect to +the AWS IoT service: + + * MQTT (over TLS 1.2) with X.509 certificate-based mutual authentication + * MQTT over WebSocket with AWS Signature Version 4 authentication + +For MQTT over TLS (port 8883), a valid certificate and private key are required +for authentication. For MQTT over WebSocket (port 443), a valid AWS Identity and Access Management (IAM) +access key ID and secret access key pair is required for authentication. + +### Thing Shadows +A thing shadow represents the cloud counterpart of a physical device or thing. +Although a device is not always online, its thing shadow is. A thing shadow +stores data in and out of the device in a JSON based document. When the device is offline, its shadow document is still +accessible to the application. When the device comes back online, +the thing shadow publishes the delta to the device (which the device didn't +see while it was offline). + +The SDK implements the protocol for applications to retrieve, update, and +delete shadow documents mentioned [here][aws-iot-thing]. +When you use the simplified access model, you have the option to enable strict document versioning. To reduce the overhead of subscribing to shadow topics +for each method requested, the SDK automatically subscribes to all of the method +topics when a connection is established. + +#### Simplified Shadow Access Model +Unlike the shadow methods, which operate on JSON documents, the simplified +shadow access model allows developers to access their shadows with getter and +setter methods. + +To use this feature, you must extend the device class ```AWSIotDevice```, +use the annotation ```AWSIotDeviceProperty``` to mark class member variables to be +managed by the SDK, and provide getter and setter methods for accessing these +variables. The getter methods will be used by the SDK to report to the shadow +periodically. The setter methods will be invoked whenever there is a change +to the desired state of the shadow document. For more information, see [Use the SDK](#use-the-sdk) +later in this document. + +## Install the SDK + +### Minimum Requirements +To use the SDK, you will need Java 1.7+. + +### Install the SDK Using Maven +The recommended way to use the AWS IoT Device SDK for Java in your project is +to consume it from Maven. Simply add the following dependency to the POM file +of your Maven project. + +```xml + + + com.amazonaws + aws-iot-device-sdk-java + 1.0.0 + + +``` + +The sample applications included with the SDK can also be installed using the following dependency definition. + +```xml + + + com.amazonaws + aws-iot-device-sdk-java-samples + 1.0.0 + + +``` + +### Install the SDK Using the Latest JAR +The latest JAR files can be downloaded [here][latest-jar]. You can simply extract +and copy the JAR files to your project's library directory, and then update your IDE to +include them to your library build path. + +You will also need to add two libraries the SDK depends on: + + * Jackson 2.x, including Jackson-core and Jackson-databind. [download instructions][jackson-download] + * Paho MQTT client for Java 1.0.x. [download instructions][paho-mqtt-java-download] + (Note: If you plan to use MQTT over WebSocket connections, use its development branch or snapshot builds here + https://repo.eclipse.org/content/repositories/paho-snapshots/.) + +### Build the SDK from the GitHub Source +You can build both the SDK and its sample applications from the source +hosted at GitHub. + +```sh +$ git clone https://github.com/aws/aws-iot-device-sdk-java.git +$ cd aws-iot-device-sdk-java +$ mvn clean install -Dgpg.skip=true +``` + +## Use the SDK +The following sections provide some basic examples of using the SDK to access the +AWS IoT service over MQTT. For more information about each API, see the [API documentation][api-docs]. + +### Initialize the Client +To access the AWS IoT service, you must initialize ```AWSIotMqttClient```. The +way in which you initialize the client depends on the connection +type (MQTT or MQTT over WebSocket) you choose. In both cases, +a valid client endpoint and client ID are required for setting up the connection. + +* Initialize the Client with MQTT (over TLS 1.2): +For this MQTT connection type (port 8883), the AWS IoT service requires TLS +mutual authentication, so a valid client certificate (X.509) +and RSA keys are required. You can use the +[AWS IoT console][aws-iot-console] or the AWS command line tools to generate certificates and keys. For the SDK, +only a certificate file and private key file are required. + +```java +String clientEndpoint = ".iot..amazonaws.com"; // replace and with your own +String clientId = ""; // replace with your own client ID. Use unique client IDs for concurrent connections. +String certificateFile = ""; // X.509 based certificate file +String privateKeyFile = ""; // PKCS#1 or PKCS#8 PEM encoded private key file + +// SampleUtil.java and its dependency PrivateKeyReader.java can be copied from the sample source code. +// Alternatively, you could load key store directly from a file - see the example included in this README. +KeyStorePasswordPair pair = SampleUtil.getKeyStorePasswordPair(certificateFile, privateKeyFile); +AWSIotMqttClient client = new AWSIotMqttClient(clientEndpoint, clientId, pair.keyStore, pair.keyPassword); + +// optional parameters can be set before connect() +client.connect(); +``` + +* Initialize the Client with MQTT Over WebSocket: +For this MQTT connection type (port 443), you will need valid IAM credentials +to initialize the client. This includes an AWS access key ID and secret +access key. There are a number of ways to get IAM credentials (for example, by creating +permanent IAM users or by requesting temporary credentials through the Amazon Cognito +service). For more information, see the developer guides for these services. + +As a best practice for application security, do not embed +credentials directly in the source code. + +```java +String clientEndpoint = ".iot..amazonaws.com"; // replace and with your own +String clientId = ""; // replace with your own client ID. Use unique client IDs for concurrent connections. + +// AWS IAM credentials could be retrieved from AWS Cognito, STS, or other secure sources +AWSIotMqttClient client = new AWSIotMqttClient(clientEndpoint, clientId, awsAccessKeyId, awsSecretAccessKey, sessionToken); + +// optional parameters can be set before connect() +client.connect(); +``` + +### Publish and Subscribe +After the client is initialized and connected, you can publish messages and subscribe +to topics. + +To publish a message using a blocking API: + +```java +String topic = "my/own/topic"; +String payload = "any payload"; + +client.publish(topic, AWSIotQos.QOS0, payload); +``` + +To publish a message using a non-blocking API: + +```java +public class MyMessage extends AWSIotMessage { + public MyMessage(String topic, AWSIotQos qos, String payload) { + super(topic, qos, payload); + } + + @Override + public void onSuccess() { + // called when message publishing succeeded + } + + @Override + public void onFailure() { + // called when message publishing failed + } + + @Override + public void onTimeout() { + // called when message publishing timed out + } +} + +String topic = "my/own/topic"; +AWSIotQos qos = AWSIotQos.QOS0; +String payload = "any payload"; +long timeout = 3000; // milliseconds + +MyMessage message = new MyMessage(topic, qos, payload); +client.publish(message, timeout); +``` + +To subscribe to a topic: + +```java +public class MyTopic extends AWSIotTopic { + public MyOwnMessage(String topic, AWSIotQos qos) { + super(topic, qos); + } + + @Override + public void onMessage(AWSIotMessage message) { + // called when a message is received + } +} + +String topic = "my/own/topic"; +AWSIotQos qos = AWSIotQos.QOS0; + +MyTopic topic = new MyTopic(topic, qos); +client.subscribe(topic); +``` + +### Shadow Methods +To access a shadow using a blocking API: + +```java +String thingName = ""; // replace with your AWS IoT Thing name + +AWSIotDevice device = new AWSIotDevice(thingName); + +client.attach(device); +client.connect(); + +// Delete existing shadow document +device.delete(); + +// Update shadow document +State state = "{\"state\":{\"reported\":{\"sensor\":3.0}}}"; +device.update(state); + +// Get the entire shadow document +String state = device.get(); +``` + +To access a shadow using a non-blocking API: + +```java +public class MyShadowMessage extends AWSIotMessage { + public MyMessage() { + super(null, null); + } + + @Override + public void onSuccess() { + // called when the shadow method succeeded + // state (JSON document) received is available in the payload field + } + + @Override + public void onFailure() { + // called when the shadow method failed + } + + @Override + public void onTimeout() { + // called when the shadow method timed out + } +} + +String thingName = ""; // replace with your AWS IoT Thing name + +AWSIotDevice device = new AWSIotDevice(thingName); + +client.attach(device); +client.connect(); + +MyShadowMessage message = new MyShadowMessage(); +long timeout = 3000; // milliseconds +device.get(message, timeout); +``` + +### Simplified Shadow Access Model +To use the simplified shadow access model, you need to extend the device class +```AWSIotDevice```, and then use the annotation class ```AWSIotDeviceProperty``` +to mark the device attributes and provide getter and setter methods for them. +The following very simple example has one attribute, ```someValue```, defined. +The code will report the attribute to the shadow, identified by ***thingName*** +every 5 seconds, in the ***reported*** section of the shadow document. The SDK +will call the setter method ```setSomeValue()``` whenever there's +a change to the ***desired*** section of the shadow document. + +```java +public class MyDevice extends AWSIotDevice { + public MyDevice(String thingName) { + super(thingName); + } + + @AWSIotDeviceProperty + private String someValue; + + public String getSomeValue() { + // read from the physical device + } + + public void setSomeValue(String newValue) { + // write to the physical device + } +} + +MyDevice device = new MyDevice(thingName); + +long reportInterval = 5000; // milliseconds. Default +interval is 3000. +device.setReportInterval(reportInterval); + +client.attach(device); +client.connect(); +``` + +### Other Topics +#### Enable Logging +The SDK uses ```java.util.logging``` for logging. To change +the logging behavior (for example, to change the logging level or logging destination), you can +specify a property file using the JVM property +```java.util.logging.config.file```. It can be provided through JVM arguments like so: + +```sh +-Djava.util.logging.config.file="logging.properties" +``` + +To change the console logging level, the property file ***logging.properties*** +should contain the following lines: + +``` +# Override of console logging level +java.util.logging.ConsoleHandler.level=INFO +``` + +#### Load KeyStore from File to Initialize the Client +You can load a KeyStore object directly from JKS-based keystore files. +You will first need to import X.509 certificate and the private key into the keystore +file like so: + +```sh +$ openssl pkcs12 -export -in -inkey -out p12.keystore -name alias +(type in the export password) + +$ keytool -importkeystore -srckeystore p12.keystore -srcstoretype PKCS12 -srcstorepass -alias alias -deststorepass -destkeypass -destkeystore my.keystore +``` + +After the keystore file ***my.keystore*** is created, you can use it to +initialize the client like so: + +```java +String keyStoreFile = ""; // replace with your own key store file +String keyStorePassword = ""; // replace with your own key store password +String keyPassword = "" // replace with your own key password + +KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); +keyStore.load(new FileInputStream(keyStoreFile), keyStorePassword.toCharArray()); + +String clientEndpoint = ".iot..amazonaws.com"; // replace and with your own +String clientId = ""; // replace with your own client ID. Use unique client IDs for concurrent connections. + +AWSIotMqttClient client = new AWSIotMqttClient(clientEndpoint, clientId, keyStore, keyPassword); +``` + +#### Use ECC-Based Certificates +You can use Elliptic Curve Cryptography (ECC)-based certificates to initialize the client. To create an ECC key and certificate, see [this blog post][aws-iot-ecc-blog]. After you have created and registered the key and certificate, use the following command to convert +the ECC key file to PKCK#8 format. + +```sh +$ openssl pkcs8 -topk8 -nocrypt -in ecckey.key -out ecckey-pk8.key +(type in the key password) +``` + +You can then use the instruction described in [this section](#initialize-the-client) to initialize the client +with just this one change. + +```java +// SampleUtil.java and its dependency PrivateKeyReader.java can be copied from the sample source code. +// Alternatively, you could load key store directly from a file - see the example included in this README. +KeyStorePasswordPair pair = SampleUtil.getKeyStorePasswordPair(certificateFile, privateKeyFile, "EC"); +``` + +## Sample Applications +There are three samples applications included with the SDK. The easiest way to +run these samples is through Maven, which will take care of getting the +dependencies. + +* Publish/Subscribe sample: +This sample consists of two publishers publishing one message per second to a +topic. One subscriber subscribing to the same topic receives and prints the +messages. + +* Shadow sample: +This sample consists of a simple demo of the simplified shadow access +model. The device contains two attributes: window state and room temperature. +Window state can be modified (therefore, controlled) remotely through +***desired*** state. To demonstrate this control function, you can use the AWS +IoT console to modify the desired window state, and then see its change from the +sample output. + +* Shadow echo sample: +This sample consists of a simple demo that uses Shadow methods to send a shadow +update and then retrieve it back every second. + +### Arguments for the Sample Applications +To run the samples, you will also need to provide the following arguments +through the command line: + +* clientEndpoint: client endpoint, in the form of ```.iot..amazonaws.com``` +* clientId: client ID +* thingName: AWS IoT thing name (not required for the Publish/Subscribe sample) + +You will also need to private either set of the following arguments for authentication. +For an MQTT connection, provide these arguments: + +* certificateFile: X.509 based certificate file +* privateKeyFile: private key file +* keyAlgorithm: (optional) RSA or EC. If not specified, RSA is used. + +For an MQTT over WebSocket connection, provide these arguments: + +* awsAccessKeyId: IAM access key ID +* awsSecretAccessKey: IAM secret access key +* sessionToken: (optional) if temporary credentials are used + +### Run the Sample Applications +You can use the following commands to execute the sample applications (assuming +TLS mutual authentication is used). + +* To run the Publish/Subscribe sample, use the following command: +```sh +$ mvn exec:java -pl aws-iot-device-sdk-java-samples -Dexec.mainClass="com.amazonaws.services.iot.client.sample.pubSub.PublishSubscribeSample" -Dexec.args="-clientEndpoint .iot..amazonaws.com -clientId -certificateFile -privateKeyFile " +``` + +* To run the Shadow sample, use the following command: +```sh +$ mvn exec:java -pl aws-iot-device-sdk-java-samples -Dexec.mainClass="com.amazonaws.services.iot.client.sample.shadow.ShadowSample" -Dexec.args="-clientEndpoint .iot..amazonaws.com -clientId -thingName -certificateFile -privateKeyFile " +``` + +* To run the Shadow echo sample, use the following command: +```sh +$ mvn exec:java -pl aws-iot-device-sdk-java-samples -Dexec.mainClass="com.amazonaws.services.iot.client.sample.shadowEcho.ShadowEchoSample" -Dexec.args="-clientEndpoint .iot..amazonaws.com -clientId -thingName -certificateFile -privateKeyFile " +``` + +### Sample Source Code +You can get the sample source code either from the GitHub repository as described +[here](#build-the-sdk-from-the-github-source) or from [the latest SDK binary][latest-jar]. +They both provide you with Maven project files that you can use to build and run the samples +from the command line or import them into an IDE, such as Eclipse. + +The sample source code included with the latest SDK binary is shipped with a modified Maven +project file (pom.xml) that allows you to build the sample source indepedently, without the +need to reference the parent POM file as with the GitHub source tree. + +## API Documentation +You'll find the API documentation for the SDK [here][api-docs]. + +## License +This SDK is distributed under the [Apache License, Version 2.0][apache-license-2]. For more information, see +LICENSE.txt and NOTICE.txt. + +## Support +If you have technical questions about the AWS IoT Device SDK, use the [AWS IoT Forum][aws-iot-forum]. +For any other questions about AWS IoT, contact [AWS Support][aws-support]. + +[aws-iot-protocol]: http://docs.aws.amazon.com/iot/latest/developerguide/protocols.html +[aws-iot-thing]: http://docs.aws.amazon.com/iot/latest/developerguide/iot-thing-shadows.html +[aws-iot-forum]: https://forums.aws.amazon.com/forum.jspa?forumID=210 +[aws-iot-console]: http://aws.amazon.com/iot/ +[latest-jar]: https://s3.amazonaws.com/aws-iot-device-sdk-java/aws-iot-device-sdk-java-LATEST.zip +[jackson-download]: http://wiki.fasterxml.com/JacksonDownload +[paho-mqtt-java-download]: https://eclipse.org/paho/clients/java/ +[api-docs]: http://aws-iot-device-sdk-java-docs.s3-website-us-east-1.amazonaws.com/ +[aws-iot-ecc-blog]: https://aws.amazon.com/blogs/iot/elliptic-curve-cryptography-and-forward-secrecy-support-in-aws-iot-3/ +[aws-support]: https://aws.amazon.com/contact-us +[apache-license-2]: http://www.apache.org/licenses/LICENSE-2.0 \ No newline at end of file diff --git a/aws-iot-device-sdk-java-samples/pom.xml b/aws-iot-device-sdk-java-samples/pom.xml new file mode 100644 index 00000000000..97e4b60630c --- /dev/null +++ b/aws-iot-device-sdk-java-samples/pom.xml @@ -0,0 +1,59 @@ + + 4.0.0 + + com.amazonaws + aws-iot-device-sdk-java-pom + 1.0.0 + + aws-iot-device-sdk-java-samples + + + com.amazonaws + aws-iot-device-sdk-java + 1.0.0 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + com/amazonaws/services/iot/client/sample/odin/*.java + + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.0 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.3 + + *.odin.* + + + + attach-javadocs + + jar + + + + + + + diff --git a/aws-iot-device-sdk-java-samples/samples-pom.xml b/aws-iot-device-sdk-java-samples/samples-pom.xml new file mode 100644 index 00000000000..263c1b46314 --- /dev/null +++ b/aws-iot-device-sdk-java-samples/samples-pom.xml @@ -0,0 +1,45 @@ + + 4.0.0 + com.amazonaws + aws-iot-device-sdk-java-samples + 1.0.0 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + com.amazonaws + aws-iot-device-sdk-java + 1.0.0 + + + com.fasterxml.jackson.core + jackson-core + 2.7.4 + + + com.fasterxml.jackson.core + jackson-databind + 2.7.4 + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + [1.0.2,) + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + + + + + diff --git a/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/pubSub/NonBlockingPublishListener.java b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/pubSub/NonBlockingPublishListener.java new file mode 100644 index 00000000000..772dd8d92ce --- /dev/null +++ b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/pubSub/NonBlockingPublishListener.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.sample.pubSub; + +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.AWSIotQos; + +/** + * This class extends {@link AWSIotMessage} to provide customized handlers for + * non-blocking message publishing. + */ +public class NonBlockingPublishListener extends AWSIotMessage { + + public NonBlockingPublishListener(String topic, AWSIotQos qos, String payload) { + super(topic, qos, payload); + } + + @Override + public void onSuccess() { + System.out.println(System.currentTimeMillis() + ": >>> " + getStringPayload()); + } + + @Override + public void onFailure() { + System.out.println(System.currentTimeMillis() + ": publish failed for " + getStringPayload()); + } + + @Override + public void onTimeout() { + System.out.println(System.currentTimeMillis() + ": publish timeout for " + getStringPayload()); + } + +} diff --git a/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/pubSub/PublishSubscribeSample.java b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/pubSub/PublishSubscribeSample.java new file mode 100644 index 00000000000..b86345a8de6 --- /dev/null +++ b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/pubSub/PublishSubscribeSample.java @@ -0,0 +1,152 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.sample.pubSub; + +import com.amazonaws.services.iot.client.AWSIotMqttClient; +import com.amazonaws.services.iot.client.AWSIotException; +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.AWSIotQos; +import com.amazonaws.services.iot.client.AWSIotTimeoutException; +import com.amazonaws.services.iot.client.AWSIotTopic; +import com.amazonaws.services.iot.client.sample.sampleUtil.CommandArguments; +import com.amazonaws.services.iot.client.sample.sampleUtil.SampleUtil; +import com.amazonaws.services.iot.client.sample.sampleUtil.SampleUtil.KeyStorePasswordPair; + +/** + * This is an example that uses {@link AWSIotMqttClient} to subscribe to a topic and + * publish messages to it. Both blocking and non-blocking publishing are + * demonstrated in this example. + */ +public class PublishSubscribeSample { + + private static final String TestTopic = "sdkTest/sub"; + private static final AWSIotQos TestTopicQos = AWSIotQos.QOS0; + + private static AWSIotMqttClient awsIotClient; + + public static void setClient(AWSIotMqttClient client) { + awsIotClient = client; + } + + public static class BlockingPublisher implements Runnable { + private final AWSIotMqttClient awsIotClient; + + public BlockingPublisher(AWSIotMqttClient awsIotClient) { + this.awsIotClient = awsIotClient; + } + + @Override + public void run() { + long counter = 1; + + while (true) { + String payload = "hello from blocking publisher - " + (counter++); + try { + awsIotClient.publish(TestTopic, payload); + } catch (AWSIotException e) { + System.out.println(System.currentTimeMillis() + ": publish failed for " + payload); + } + System.out.println(System.currentTimeMillis() + ": >>> " + payload); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + System.out.println(System.currentTimeMillis() + ": BlockingPublisher was interrupted"); + return; + } + } + } + } + + public static class NonBlockingPublisher implements Runnable { + private final AWSIotMqttClient awsIotClient; + + public NonBlockingPublisher(AWSIotMqttClient awsIotClient) { + this.awsIotClient = awsIotClient; + } + + @Override + public void run() { + long counter = 1; + + while (true) { + String payload = "hello from non-blocking publisher - " + (counter++); + AWSIotMessage message = new NonBlockingPublishListener(TestTopic, TestTopicQos, payload); + try { + awsIotClient.publish(message); + } catch (AWSIotException e) { + System.out.println(System.currentTimeMillis() + ": publish failed for " + payload); + } + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + System.out.println(System.currentTimeMillis() + ": NonBlockingPublisher was interrupted"); + return; + } + } + } + } + + private static void initClient(CommandArguments arguments) { + String clientEndpoint = arguments.getNotNull("clientEndpoint", SampleUtil.getConfig("clientEndpoint")); + String clientId = arguments.getNotNull("clientId", SampleUtil.getConfig("clientId")); + + String certificateFile = arguments.get("certificateFile", SampleUtil.getConfig("certificateFile")); + String privateKeyFile = arguments.get("privateKeyFile", SampleUtil.getConfig("privateKeyFile")); + if (awsIotClient == null && certificateFile != null && privateKeyFile != null) { + String algorithm = arguments.get("keyAlgorithm", SampleUtil.getConfig("keyAlgorithm")); + KeyStorePasswordPair pair = SampleUtil.getKeyStorePasswordPair(certificateFile, privateKeyFile, algorithm); + + awsIotClient = new AWSIotMqttClient(clientEndpoint, clientId, pair.keyStore, pair.keyPassword); + } + + if (awsIotClient == null) { + String awsAccessKeyId = arguments.get("awsAccessKeyId", SampleUtil.getConfig("awsAccessKeyId")); + String awsSecretAccessKey = arguments.get("awsSecretAccessKey", SampleUtil.getConfig("awsSecretAccessKey")); + String sessionToken = arguments.get("sessionToken", SampleUtil.getConfig("sessionToken")); + + if (awsAccessKeyId != null && awsSecretAccessKey != null) { + awsIotClient = new AWSIotMqttClient(clientEndpoint, clientId, awsAccessKeyId, awsSecretAccessKey, + sessionToken); + } + } + + if (awsIotClient == null) { + throw new IllegalArgumentException("Failed to construct client due to missing certificate or credentials."); + } + } + + public static void main(String args[]) throws InterruptedException, AWSIotException, AWSIotTimeoutException { + CommandArguments arguments = CommandArguments.parse(args); + initClient(arguments); + + awsIotClient.connect(); + + AWSIotTopic topic = new TestTopicListener(TestTopic, TestTopicQos); + awsIotClient.subscribe(topic, true); + + Thread blockingPublishThread = new Thread(new BlockingPublisher(awsIotClient)); + Thread nonBlockingPublishThread = new Thread(new NonBlockingPublisher(awsIotClient)); + + blockingPublishThread.start(); + nonBlockingPublishThread.start(); + + blockingPublishThread.join(); + nonBlockingPublishThread.join(); + } + +} diff --git a/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/pubSub/TestTopicListener.java b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/pubSub/TestTopicListener.java new file mode 100644 index 00000000000..d3069ad9cf9 --- /dev/null +++ b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/pubSub/TestTopicListener.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.sample.pubSub; + +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.AWSIotQos; +import com.amazonaws.services.iot.client.AWSIotTopic; + +/** + * This class extends {@link AWSIotTopic} to receive messages from a subscribed + * topic. + */ +public class TestTopicListener extends AWSIotTopic { + + public TestTopicListener(String topic, AWSIotQos qos) { + super(topic, qos); + } + + @Override + public void onMessage(AWSIotMessage message) { + System.out.println(System.currentTimeMillis() + ": <<< " + message.getStringPayload()); + } + +} diff --git a/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/sampleUtil/CommandArguments.java b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/sampleUtil/CommandArguments.java new file mode 100644 index 00000000000..10609637c3d --- /dev/null +++ b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/sampleUtil/CommandArguments.java @@ -0,0 +1,90 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.sample.sampleUtil; + +import java.util.HashMap; +import java.util.Map; + +public class CommandArguments { + + private final Map arguments = new HashMap(); + + private CommandArguments(String[] args) { + String name = null; + + for (int i = 0; i < args.length; i++) { + String arg = args[i].trim(); + if (name == null) { + if (arg.startsWith("-")) { + name = arg.replaceFirst("^-+", ""); + if (name.length() < 1) { + name = null; + } + } + continue; + } + + if (arg.startsWith("-")) { + arguments.put(name.toLowerCase(), null); + + name = arg.replaceFirst("^-+", ""); + if (name.length() < 1) { + name = null; + } + } else { + arguments.put(name.toLowerCase(), arg); + name = null; + } + } + + if (name != null) { + arguments.put(name.toLowerCase(), null); + } + } + + public static CommandArguments parse(String[] args) { + return new CommandArguments(args); + } + + public String get(String name) { + return arguments.get(name.toLowerCase()); + } + + public String get(String name, String defaultValue) { + String value = arguments.get(name.toLowerCase()); + if (value == null) { + value = defaultValue; + } + return value; + } + + public String getNotNull(String name) { + String value = get(name); + if (value == null) { + throw new RuntimeException("Missing required argumment for " + name); + } + return value; + } + + public String getNotNull(String name, String defaultValue) { + String value = get(name, defaultValue); + if (value == null) { + throw new RuntimeException("Missing required argumment for " + name); + } + return value; + } + +} diff --git a/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/sampleUtil/PrivateKeyReader.java b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/sampleUtil/PrivateKeyReader.java new file mode 100644 index 00000000000..dd4be85e2ff --- /dev/null +++ b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/sampleUtil/PrivateKeyReader.java @@ -0,0 +1,491 @@ +/**************************************************************************** + * Amazon Modifications: Copyright 2016 Amazon.com, Inc. or its affiliates. + * All Rights Reserved. + ***************************************************************************** + * Copyright (c) 1998-2010 AOL Inc. + * + * 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. + * + ****************************************************************************/ +// http://oauth.googlecode.com/svn/code/branches/jmeter/jmeter/src/main/java/org/apache/jmeter/protocol/oauth/sampler/PrivateKeyReader.java + +package com.amazonaws.services.iot.client.sample.sampleUtil; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; + +import javax.xml.bind.DatatypeConverter; + +/** + * Class for reading RSA or ECC private key from PEM file. + * + * It can read PEM files with PKCS#8 or PKCS#1 encodings. It doesn't support + * encrypted PEM files. + */ +public class PrivateKeyReader { + + // Private key file using PKCS #1 encoding + public static final String P1_BEGIN_MARKER = "-----BEGIN RSA PRIVATE KEY"; //$NON-NLS-1$ + public static final String P1_END_MARKER = "-----END RSA PRIVATE KEY"; //$NON-NLS-1$ + + // Private key file using PKCS #8 encoding + public static final String P8_BEGIN_MARKER = "-----BEGIN PRIVATE KEY"; //$NON-NLS-1$ + public static final String P8_END_MARKER = "-----END PRIVATE KEY"; //$NON-NLS-1$ + + /** + * Get a RSA Private Key from InputStream. + * + * @param fileName + * file name + * @return Private key + * @throws IOException + * IOException resulted from invalid file IO + * @throws GeneralSecurityException + * GeneralSecurityException resulted from invalid key format + */ + public static PrivateKey getPrivateKey(String fileName) throws IOException, GeneralSecurityException { + try (InputStream stream = new FileInputStream(fileName)) { + return getPrivateKey(stream, null); + } + } + + /** + * Get a Private Key from InputStream. + * + * @param fileName + * file name + * @param algorithm + * the name of the key algorithm, for example "RSA" or "EC" + * @return Private key + * @throws IOException + * IOException resulted from invalid file IO + * @throws GeneralSecurityException + * GeneralSecurityException resulted from invalid key data + */ + public static PrivateKey getPrivateKey(String fileName, String algorithm) throws IOException, + GeneralSecurityException { + try (InputStream stream = new FileInputStream(fileName)) { + return getPrivateKey(stream, algorithm); + } + } + + /** + * Get a Private Key for the file. + * + * @param stream + * InputStream object + * @param algorithm + * the name of the key algorithm, for example "RSA" or "EC" + * @return Private key + * @throws IOException + * IOException resulted from invalid file IO + * @throws GeneralSecurityException + * GeneralSecurityException resulted from invalid key data + */ + public static PrivateKey getPrivateKey(InputStream stream, String algorithm) throws IOException, + GeneralSecurityException { + PrivateKey key = null; + boolean isRSAKey = false; + + BufferedReader br = new BufferedReader(new InputStreamReader(stream, "UTF-8")); + StringBuilder builder = new StringBuilder(); + boolean inKey = false; + for (String line = br.readLine(); line != null; line = br.readLine()) { + if (!inKey) { + if (line.startsWith("-----BEGIN ") && line.endsWith(" PRIVATE KEY-----")) { + inKey = true; + isRSAKey = line.contains("RSA"); + } + continue; + } else { + if (line.startsWith("-----END ") && line.endsWith(" PRIVATE KEY-----")) { + inKey = false; + isRSAKey = line.contains("RSA"); + break; + } + builder.append(line); + } + } + KeySpec keySpec = null; + byte[] encoded = DatatypeConverter.parseBase64Binary(builder.toString()); + if (isRSAKey) { + keySpec = getRSAKeySpec(encoded); + } else { + keySpec = new PKCS8EncodedKeySpec(encoded); + } + KeyFactory kf = KeyFactory.getInstance((algorithm == null) ? "RSA" : algorithm); + key = kf.generatePrivate(keySpec); + + return key; + } + + /** + * Convert PKCS#1 encoded private key into RSAPrivateCrtKeySpec. + * + *

+ * The ASN.1 syntax for the private key with CRT is + * + *

+     * -- 
+     * -- Representation of RSA private key with information for the CRT algorithm.
+     * --
+     * RSAPrivateKey ::= SEQUENCE {
+     *   version           Version, 
+     *   modulus           INTEGER,  -- n
+     *   publicExponent    INTEGER,  -- e
+     *   privateExponent   INTEGER,  -- d
+     *   prime1            INTEGER,  -- p
+     *   prime2            INTEGER,  -- q
+     *   exponent1         INTEGER,  -- d mod (p-1)
+     *   exponent2         INTEGER,  -- d mod (q-1) 
+     *   coefficient       INTEGER,  -- (inverse of q) mod p
+     *   otherPrimeInfos   OtherPrimeInfos OPTIONAL 
+     * }
+     * 
+ * + * @param keyBytes + * PKCS#1 encoded key + * @return KeySpec + * @throws IOException + * IOException resulted from invalid file IO + */ + private static RSAPrivateCrtKeySpec getRSAKeySpec(byte[] keyBytes) throws IOException { + + DerParser parser = new DerParser(keyBytes); + + Asn1Object sequence = parser.read(); + if (sequence.getType() != DerParser.SEQUENCE) + throw new IOException("Invalid DER: not a sequence"); //$NON-NLS-1$ + + // Parse inside the sequence + parser = sequence.getParser(); + + parser.read(); // Skip version + BigInteger modulus = parser.read().getInteger(); + BigInteger publicExp = parser.read().getInteger(); + BigInteger privateExp = parser.read().getInteger(); + BigInteger prime1 = parser.read().getInteger(); + BigInteger prime2 = parser.read().getInteger(); + BigInteger exp1 = parser.read().getInteger(); + BigInteger exp2 = parser.read().getInteger(); + BigInteger crtCoef = parser.read().getInteger(); + + RSAPrivateCrtKeySpec keySpec = new RSAPrivateCrtKeySpec(modulus, publicExp, privateExp, prime1, prime2, exp1, + exp2, crtCoef); + + return keySpec; + } +} + +/** + * A bare-minimum ASN.1 DER decoder, just having enough functions to decode + * PKCS#1 private keys. Especially, it doesn't handle explicitly tagged types + * with an outer tag. + * + *

+ * This parser can only handle one layer. To parse nested constructs, get a new + * parser for each layer using Asn1Object.getParser(). + * + *

+ * There are many DER decoders in JRE but using them will tie this program to a + * specific JCE/JVM. + * + * @author zhang + * + */ +class DerParser { + + // Classes + public final static int UNIVERSAL = 0x00; + public final static int APPLICATION = 0x40; + public final static int CONTEXT = 0x80; + public final static int PRIVATE = 0xC0; + + // Constructed Flag + public final static int CONSTRUCTED = 0x20; + + // Tag and data types + public final static int ANY = 0x00; + public final static int BOOLEAN = 0x01; + public final static int INTEGER = 0x02; + public final static int BIT_STRING = 0x03; + public final static int OCTET_STRING = 0x04; + public final static int NULL = 0x05; + public final static int OBJECT_IDENTIFIER = 0x06; + public final static int REAL = 0x09; + public final static int ENUMERATED = 0x0a; + public final static int RELATIVE_OID = 0x0d; + + public final static int SEQUENCE = 0x10; + public final static int SET = 0x11; + + public final static int NUMERIC_STRING = 0x12; + public final static int PRINTABLE_STRING = 0x13; + public final static int T61_STRING = 0x14; + public final static int VIDEOTEX_STRING = 0x15; + public final static int IA5_STRING = 0x16; + public final static int GRAPHIC_STRING = 0x19; + public final static int ISO646_STRING = 0x1A; + public final static int GENERAL_STRING = 0x1B; + + public final static int UTF8_STRING = 0x0C; + public final static int UNIVERSAL_STRING = 0x1C; + public final static int BMP_STRING = 0x1E; + + public final static int UTC_TIME = 0x17; + public final static int GENERALIZED_TIME = 0x18; + + protected InputStream in; + + /** + * Create a new DER decoder from an input stream. + * + * @param in + * The DER encoded stream + */ + public DerParser(InputStream in) throws IOException { + this.in = in; + } + + /** + * Create a new DER decoder from a byte array. + * + * @param The + * encoded bytes + * @throws IOException + * IOException resulted from invalid file IO + */ + public DerParser(byte[] bytes) throws IOException { + this(new ByteArrayInputStream(bytes)); + } + + /** + * Read next object. If it's constructed, the value holds encoded content + * and it should be parsed by a new parser from + * Asn1Object.getParser. + * + * @return A object + * @throws IOException + * IOException resulted from invalid file IO + */ + public Asn1Object read() throws IOException { + int tag = in.read(); + + if (tag == -1) + throw new IOException("Invalid DER: stream too short, missing tag"); //$NON-NLS-1$ + + int length = getLength(); + + byte[] value = new byte[length]; + int n = in.read(value); + if (n < length) + throw new IOException("Invalid DER: stream too short, missing value"); //$NON-NLS-1$ + + Asn1Object o = new Asn1Object(tag, length, value); + + return o; + } + + /** + * Decode the length of the field. Can only support length encoding up to 4 + * octets. + * + *

+ * In BER/DER encoding, length can be encoded in 2 forms, + *

    + *
  • Short form. One octet. Bit 8 has value "0" and bits 7-1 give the + * length. + *
  • Long form. Two to 127 octets (only 4 is supported here). Bit 8 of + * first octet has value "1" and bits 7-1 give the number of additional + * length octets. Second and following octets give the length, base 256, + * most significant digit first. + *
+ * + * @return The length as integer + * @throws IOException + * IOException resulted from invalid file IO + */ + private int getLength() throws IOException { + + int i = in.read(); + if (i == -1) + throw new IOException("Invalid DER: length missing"); //$NON-NLS-1$ + + // A single byte short length + if ((i & ~0x7F) == 0) + return i; + + int num = i & 0x7F; + + // We can't handle length longer than 4 bytes + if (i >= 0xFF || num > 4) + throw new IOException("Invalid DER: length field too big (" //$NON-NLS-1$ + + i + ")"); //$NON-NLS-1$ + + byte[] bytes = new byte[num]; + int n = in.read(bytes); + if (n < num) + throw new IOException("Invalid DER: length too short"); //$NON-NLS-1$ + + return new BigInteger(1, bytes).intValue(); + } + +} + +/** + * An ASN.1 TLV. The object is not parsed. It can only handle integers and + * strings. + * + * @author zhang + * + */ +class Asn1Object { + + protected final int type; + protected final int length; + protected final byte[] value; + protected final int tag; + + /** + * Construct a ASN.1 TLV. The TLV could be either a constructed or primitive + * entity. + * + *

+ * The first byte in DER encoding is made of following fields, + * + *

+     * -------------------------------------------------
+     * |Bit 8|Bit 7|Bit 6|Bit 5|Bit 4|Bit 3|Bit 2|Bit 1|
+     * -------------------------------------------------
+     * |  Class    | CF  |     +      Type             |
+     * -------------------------------------------------
+     * 
+ * + *
    + *
  • Class: Universal, Application, Context or Private + *
  • CF: Constructed flag. If 1, the field is constructed. + *
  • Type: This is actually called tag in ASN.1. It indicates data type + * (Integer, String) or a construct (sequence, choice, set). + *
+ * + * @param tag + * Tag or Identifier + * @param length + * Length of the field + * @param value + * Encoded octet string for the field. + */ + public Asn1Object(int tag, int length, byte[] value) { + this.tag = tag; + this.type = tag & 0x1F; + this.length = length; + this.value = value; + } + + public int getType() { + return type; + } + + public int getLength() { + return length; + } + + public byte[] getValue() { + return value; + } + + public boolean isConstructed() { + return (tag & DerParser.CONSTRUCTED) == DerParser.CONSTRUCTED; + } + + /** + * For constructed field, return a parser for its content. + * + * @return A parser for the construct. + * @throws IOException + * IOException resulted from invalid file IO + */ + public DerParser getParser() throws IOException { + if (!isConstructed()) + throw new IOException("Invalid DER: can't parse primitive entity"); //$NON-NLS-1$ + + return new DerParser(value); + } + + /** + * Get the value as integer + * + * @return BigInteger + * @throws IOException + * IOException resulted from invalid file IO + */ + public BigInteger getInteger() throws IOException { + if (type != DerParser.INTEGER) + throw new IOException("Invalid DER: object is not integer"); //$NON-NLS-1$ + + return new BigInteger(value); + } + + /** + * Get value as string. Most strings are treated as Latin-1. + * + * @return Java string + * @throws IOException + * IOException resulted from invalid file IO + */ + public String getString() throws IOException { + + String encoding; + + switch (type) { + + // Not all are Latin-1 but it's the closest thing + case DerParser.NUMERIC_STRING: + case DerParser.PRINTABLE_STRING: + case DerParser.VIDEOTEX_STRING: + case DerParser.IA5_STRING: + case DerParser.GRAPHIC_STRING: + case DerParser.ISO646_STRING: + case DerParser.GENERAL_STRING: + encoding = "ISO-8859-1"; //$NON-NLS-1$ + break; + + case DerParser.BMP_STRING: + encoding = "UTF-16BE"; //$NON-NLS-1$ + break; + + case DerParser.UTF8_STRING: + encoding = "UTF-8"; //$NON-NLS-1$ + break; + + case DerParser.UNIVERSAL_STRING: + throw new IOException("Invalid DER: can't handle UCS-4 string"); //$NON-NLS-1$ + + default: + throw new IOException("Invalid DER: object is not a string"); //$NON-NLS-1$ + } + + return new String(value, encoding); + } +} \ No newline at end of file diff --git a/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/sampleUtil/SampleUtil.java b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/sampleUtil/SampleUtil.java new file mode 100644 index 00000000000..e11dca30fb6 --- /dev/null +++ b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/sampleUtil/SampleUtil.java @@ -0,0 +1,147 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.sample.sampleUtil; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.Properties; + +/** + * This is a helper class to facilitate reading of the configurations and + * certificate from the resource files. + */ +public class SampleUtil { + private static final String PropertyFile = "aws-iot-sdk-samples.properties"; + + public static class KeyStorePasswordPair { + public KeyStore keyStore; + public String keyPassword; + + public KeyStorePasswordPair(KeyStore keyStore, String keyPassword) { + this.keyStore = keyStore; + this.keyPassword = keyPassword; + } + } + + public static String getConfig(String name) { + Properties prop = new Properties(); + URL resource = SampleUtil.class.getResource(PropertyFile); + if (resource == null) { + return null; + } + try (InputStream stream = resource.openStream()) { + prop.load(stream); + } catch (IOException e) { + return null; + } + String value = prop.getProperty(name); + if (value == null || value.trim().length() == 0) { + return null; + } else { + return value; + } + } + + public static KeyStorePasswordPair getKeyStorePasswordPair(String certificateFile, String privateKeyFile) { + return getKeyStorePasswordPair(certificateFile, privateKeyFile, null); + } + + public static KeyStorePasswordPair getKeyStorePasswordPair(String certificateFile, String privateKeyFile, + String keyAlgorithm) { + if (certificateFile == null || privateKeyFile == null) { + System.out.println("Certificate or private key file missing"); + return null; + } + + Certificate certificate = loadCertificateFromFile(certificateFile); + PrivateKey privateKey = loadPrivateKeyFromFile(privateKeyFile, keyAlgorithm); + if (certificate == null || privateKey == null) { + return null; + } + + return getKeyStorePasswordPair(certificate, privateKey); + } + + public static KeyStorePasswordPair getKeyStorePasswordPair(Certificate certificate, PrivateKey privateKey) { + KeyStore keyStore = null; + String keyPassword = null; + try { + keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + keyStore.setCertificateEntry("alias", certificate); + + // randomly generated key password for the key in the KeyStore + keyPassword = new BigInteger(128, new SecureRandom()).toString(32); + keyStore.setKeyEntry("alias", privateKey, keyPassword.toCharArray(), new Certificate[] { certificate }); + } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) { + System.out.println("Failed to create key store"); + return null; + } + + return new KeyStorePasswordPair(keyStore, keyPassword); + } + + private static Certificate loadCertificateFromFile(String filename) { + Certificate certificate = null; + + File file = new File(filename); + if (!file.exists()) { + System.out.println("Certificate file not found: " + filename); + return null; + } + try (BufferedInputStream stream = new BufferedInputStream(new FileInputStream(file))) { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + certificate = certFactory.generateCertificate(stream); + } catch (IOException | CertificateException e) { + System.out.println("Failed to load certificate file " + filename); + } + + return certificate; + } + + private static PrivateKey loadPrivateKeyFromFile(String filename, String algorithm) { + PrivateKey privateKey = null; + + File file = new File(filename); + if (!file.exists()) { + System.out.println("Private key file not found: " + filename); + return null; + } + try (DataInputStream stream = new DataInputStream(new FileInputStream(file))) { + privateKey = PrivateKeyReader.getPrivateKey(stream, algorithm); + } catch (IOException | GeneralSecurityException e) { + System.out.println("Failed to load private key from file " + filename); + } + + return privateKey; + } + +} diff --git a/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/shadow/ConnectedWindow.java b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/shadow/ConnectedWindow.java new file mode 100644 index 00000000000..9b29f222aad --- /dev/null +++ b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/shadow/ConnectedWindow.java @@ -0,0 +1,77 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.sample.shadow; + +import java.util.Random; + +import com.amazonaws.services.iot.client.AWSIotDevice; +import com.amazonaws.services.iot.client.AWSIotDeviceProperty; + +/** + * This class encapsulates an actual device. It extends {@link AWSIotDevice} to + * define properties that are to be kept in sync with the AWS IoT shadow. + */ +public class ConnectedWindow extends AWSIotDevice { + + public ConnectedWindow(String thingName) { + super(thingName); + } + + @AWSIotDeviceProperty + private boolean windowOpen; + + @AWSIotDeviceProperty + private float roomTemperature; + + public boolean getWindowOpen() { + // 1. read the window state from the window actuator + boolean reportedState = this.windowOpen; + System.out.println( + System.currentTimeMillis() + " >>> reported window state: " + (reportedState ? "open" : "closed")); + + // 2. return the current window state + return reportedState; + } + + public void setWindowOpen(boolean desiredState) { + // 1. update the window actuator with the desired state + this.windowOpen = desiredState; + + System.out.println( + System.currentTimeMillis() + " <<< desired window state to " + (desiredState ? "open" : "closed")); + } + + public float getRoomTemperature() { + // 1. Read the actual room temperature from the thermostat + Random rand = new Random(); + float minTemperature = 20.0f; + float maxTemperature = 85.0f; + float reportedTemperature = rand.nextFloat() * (maxTemperature - minTemperature) + minTemperature; + + // 2. (optionally) update the local copy + this.roomTemperature = reportedTemperature; + + // 3. return the current room temperature + System.out.println(System.currentTimeMillis() + " >>> reported room temperature: " + reportedTemperature); + return this.roomTemperature; + } + + public void setRoomTemperature(float desiredTemperature) { + // no-op as room temperature is a read-only property. It's not required + // to have this setter. + } + +} diff --git a/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/shadow/ShadowSample.java b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/shadow/ShadowSample.java new file mode 100644 index 00000000000..89212c48fee --- /dev/null +++ b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/shadow/ShadowSample.java @@ -0,0 +1,100 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.sample.shadow; + +import com.amazonaws.services.iot.client.AWSIotConnectionStatus; +import com.amazonaws.services.iot.client.AWSIotDevice; +import com.amazonaws.services.iot.client.AWSIotException; +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.AWSIotMqttClient; +import com.amazonaws.services.iot.client.AWSIotQos; +import com.amazonaws.services.iot.client.sample.sampleUtil.CommandArguments; +import com.amazonaws.services.iot.client.sample.sampleUtil.SampleUtil; +import com.amazonaws.services.iot.client.sample.sampleUtil.SampleUtil.KeyStorePasswordPair; + +/** + * This example demonstrates how to use {@link AWSIotMqttClient} and a derived + * {@link AWSIotDevice} to keep the device connected with its shadow in the + * cloud. This is the recommended way of accessing a shadow and keeping it + * synchronized with the device. The only thing the application needs to provide + * in the derived class is the annotated device properties and their + * corresponding getter and setter methods. + */ +public class ShadowSample { + + private static AWSIotMqttClient awsIotClient; + + public static void setClient(AWSIotMqttClient client) { + awsIotClient = client; + } + + private static void initClient(CommandArguments arguments) { + String clientEndpoint = arguments.getNotNull("clientEndpoint", SampleUtil.getConfig("clientEndpoint")); + String clientId = arguments.getNotNull("clientId", SampleUtil.getConfig("clientId")); + + String certificateFile = arguments.get("certificateFile", SampleUtil.getConfig("certificateFile")); + String privateKeyFile = arguments.get("privateKeyFile", SampleUtil.getConfig("privateKeyFile")); + if (awsIotClient == null && certificateFile != null && privateKeyFile != null) { + String algorithm = arguments.get("keyAlgorithm", SampleUtil.getConfig("keyAlgorithm")); + KeyStorePasswordPair pair = SampleUtil.getKeyStorePasswordPair(certificateFile, privateKeyFile, algorithm); + + awsIotClient = new AWSIotMqttClient(clientEndpoint, clientId, pair.keyStore, pair.keyPassword); + } + + if (awsIotClient == null) { + String awsAccessKeyId = arguments.get("awsAccessKeyId", SampleUtil.getConfig("awsAccessKeyId")); + String awsSecretAccessKey = arguments.get("awsSecretAccessKey", SampleUtil.getConfig("awsSecretAccessKey")); + String sessionToken = arguments.get("sessionToken", SampleUtil.getConfig("sessionToken")); + + if (awsAccessKeyId != null && awsSecretAccessKey != null) { + awsIotClient = new AWSIotMqttClient(clientEndpoint, clientId, awsAccessKeyId, awsSecretAccessKey, + sessionToken); + } + } + + if (awsIotClient == null) { + throw new IllegalArgumentException("Failed to construct client due to missing certificate or credentials."); + } + } + + public static void main(String args[]) throws InterruptedException, AWSIotException { + CommandArguments arguments = CommandArguments.parse(args); + initClient(arguments); + + awsIotClient.setWillMessage(new AWSIotMessage("client/disconnect", AWSIotQos.QOS0, awsIotClient.getClientId())); + + String thingName = arguments.getNotNull("thingName", SampleUtil.getConfig("thingName")); + ConnectedWindow connectedWindow = new ConnectedWindow(thingName); + + awsIotClient.attach(connectedWindow); + awsIotClient.connect(); + + // Delete existing document if any + connectedWindow.delete(); + + AWSIotConnectionStatus status = AWSIotConnectionStatus.DISCONNECTED; + while (true) { + AWSIotConnectionStatus newStatus = awsIotClient.getConnectionStatus(); + if (!status.equals(newStatus)) { + System.out.println(System.currentTimeMillis() + " Connection status changed to " + newStatus); + status = newStatus; + } + + Thread.sleep(1000); + } + } + +} diff --git a/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/shadowEcho/ShadowEchoSample.java b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/shadowEcho/ShadowEchoSample.java new file mode 100644 index 00000000000..069245a0f5f --- /dev/null +++ b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/shadowEcho/ShadowEchoSample.java @@ -0,0 +1,122 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.sample.shadowEcho; + +import java.io.IOException; + +import com.amazonaws.services.iot.client.AWSIotMqttClient; +import com.amazonaws.services.iot.client.AWSIotDevice; +import com.amazonaws.services.iot.client.AWSIotException; +import com.amazonaws.services.iot.client.AWSIotTimeoutException; +import com.amazonaws.services.iot.client.sample.sampleUtil.CommandArguments; +import com.amazonaws.services.iot.client.sample.sampleUtil.SampleUtil; +import com.amazonaws.services.iot.client.sample.sampleUtil.SampleUtil.KeyStorePasswordPair; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * This example demonstrates how to use {@link AWSIotDevice} to directly access + * the shadow document. + */ +public class ShadowEchoSample { + + private static AWSIotMqttClient awsIotClient; + + public static void setClient(AWSIotMqttClient client) { + awsIotClient = client; + } + + private static void initClient(CommandArguments arguments) { + String clientEndpoint = arguments.getNotNull("clientEndpoint", SampleUtil.getConfig("clientEndpoint")); + String clientId = arguments.getNotNull("clientId", SampleUtil.getConfig("clientId")); + + String certificateFile = arguments.get("certificateFile", SampleUtil.getConfig("certificateFile")); + String privateKeyFile = arguments.get("privateKeyFile", SampleUtil.getConfig("privateKeyFile")); + if (awsIotClient == null && certificateFile != null && privateKeyFile != null) { + String algorithm = arguments.get("keyAlgorithm", SampleUtil.getConfig("keyAlgorithm")); + KeyStorePasswordPair pair = SampleUtil.getKeyStorePasswordPair(certificateFile, privateKeyFile, algorithm); + + awsIotClient = new AWSIotMqttClient(clientEndpoint, clientId, pair.keyStore, pair.keyPassword); + } + + if (awsIotClient == null) { + String awsAccessKeyId = arguments.get("awsAccessKeyId", SampleUtil.getConfig("awsAccessKeyId")); + String awsSecretAccessKey = arguments.get("awsSecretAccessKey", SampleUtil.getConfig("awsSecretAccessKey")); + String sessionToken = arguments.get("sessionToken", SampleUtil.getConfig("sessionToken")); + + if (awsAccessKeyId != null && awsSecretAccessKey != null) { + awsIotClient = new AWSIotMqttClient(clientEndpoint, clientId, awsAccessKeyId, awsSecretAccessKey, + sessionToken); + } + } + + if (awsIotClient == null) { + throw new IllegalArgumentException("Failed to construct client due to missing certificate or credentials."); + } + } + + public static void main(String args[]) throws IOException, AWSIotException, AWSIotTimeoutException, + InterruptedException { + CommandArguments arguments = CommandArguments.parse(args); + initClient(arguments); + + String thingName = arguments.getNotNull("thingName", SampleUtil.getConfig("thingName")); + AWSIotDevice device = new AWSIotDevice(thingName); + + awsIotClient.attach(device); + awsIotClient.connect(); + + // Delete existing document if any + device.delete(); + + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + Thing thing = new Thing(); + + while (true) { + long desired = thing.state.desired.counter; + thing.state.reported.counter = desired; + thing.state.desired.counter = desired + 1; + + String jsonState = objectMapper.writeValueAsString(thing); + + try { + // Send updated document to the shadow + device.update(jsonState); + System.out.println(System.currentTimeMillis() + ": >>> " + jsonState); + } catch (AWSIotException e) { + System.out.println(System.currentTimeMillis() + ": update failed for " + jsonState); + continue; + } + + try { + // Retrieve updated document from the shadow + String shadowState = device.get(); + System.out.println(System.currentTimeMillis() + ": <<< " + shadowState); + + thing = objectMapper.readValue(shadowState, Thing.class); + } catch (AWSIotException e) { + System.out.println(System.currentTimeMillis() + ": get failed for " + jsonState); + continue; + } + + Thread.sleep(1000); + } + + } + +} diff --git a/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/shadowEcho/Thing.java b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/shadowEcho/Thing.java new file mode 100644 index 00000000000..dc07d17a2d6 --- /dev/null +++ b/aws-iot-device-sdk-java-samples/src/main/java/com/amazonaws/services/iot/client/sample/shadowEcho/Thing.java @@ -0,0 +1,35 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.sample.shadowEcho; + +/** + * This POJO class defines a simple document for communicating with the AWS IoT + * thing. It only contains one property ({@code counter}). + */ +public class Thing { + + public State state = new State(); + + public static class State { + public Document reported = new Document(); + public Document desired = new Document(); + } + + public static class Document { + public long counter = 1; + } + +} diff --git a/aws-iot-device-sdk-java-samples/src/main/resources/com/amazonaws/services/iot/client/sample/sampleUtil/aws-iot-sdk-samples.properties b/aws-iot-device-sdk-java-samples/src/main/resources/com/amazonaws/services/iot/client/sample/sampleUtil/aws-iot-sdk-samples.properties new file mode 100644 index 00000000000..b42fc7b80b5 --- /dev/null +++ b/aws-iot-device-sdk-java-samples/src/main/resources/com/amazonaws/services/iot/client/sample/sampleUtil/aws-iot-sdk-samples.properties @@ -0,0 +1,14 @@ +# Client endpoint, e.g. .iot.us-east-1.amazonaws.com +clientEndpoint= + +# Client ID, unique client ID per connection +clientId= + +# Client certificate file +certificateFile= + +# Client private key file +privateKeyFile= + +# Thing name +thingName= \ No newline at end of file diff --git a/aws-iot-device-sdk-java/pom.xml b/aws-iot-device-sdk-java/pom.xml new file mode 100644 index 00000000000..a6ac904c8a6 --- /dev/null +++ b/aws-iot-device-sdk-java/pom.xml @@ -0,0 +1,106 @@ + + 4.0.0 + + com.amazonaws + aws-iot-device-sdk-java-pom + 1.0.0 + + aws-iot-device-sdk-java + + + org.mockito + mockito-all + 1.10.19 + test + + + junit + junit + 4.12 + test + + + org.projectlombok + lombok + 1.16.8 + provided + + + com.fasterxml.jackson.core + jackson-core + 2.7.4 + + + com.fasterxml.jackson.core + jackson-databind + 2.7.4 + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + [1.0.2,) + + + + target/generated-sources/delombok + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + **/odin/*.java + + + + + org.projectlombok + lombok-maven-plugin + 1.16.8.0 + + + generate-sources + + delombok + + + false + src/main/java + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.0 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.3 + + target/generated-sources/delombok + *.odin.* + + + + attach-javadocs + + jar + + + + + + + diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotConfig.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotConfig.java new file mode 100644 index 00000000000..0cdbacb4501 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotConfig.java @@ -0,0 +1,109 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client; + +/** + * The class provides default values for the library. All the values defined + * here can be overridden at runtime through setter functions in + * {@link AWSIotMqttClient} and {@link AWSIotDevice}. + */ +public class AWSIotConfig { + + /** + * The default value for number of client threads. See also + * {@link AWSIotMqttClient#getNumOfClientThreads()}. + */ + public static final int NUM_OF_CLIENT_THREADS = 1; + + /** + * The default value for client connection timeout (milliseconds). See also + * {@link AWSIotMqttClient#getConnectionTimeout()}. + */ + public static final int CONNECTION_TIMEOUT = 30000; + + /** + * The default value for service acknowledge timeout (milliseconds). See + * also {@link AWSIotMqttClient#getServerAckTimeout()}. + */ + public static final int SERVER_ACK_TIMEOUT = 3000; + + /** + * The default value for client keep-alive interval (milliseconds). See also + * {@link AWSIotMqttClient#getKeepAliveInterval()}. + */ + public static final int KEEP_ALIVE_INTERVAL = 30000; + + /** + * The default value for maximum connection retry times. See also + * {@link AWSIotMqttClient#getMaxConnectionRetries()}. + */ + public static final int MAX_CONNECTION_RETRIES = 5; + + /** + * The default value for connection base retry delay (milliseconds). See + * also {@link AWSIotMqttClient#getBaseRetryDelay()}. + */ + public static final int CONNECTION_BASE_RETRY_DELAY = 3000; + + /** + * The default value for connection maximum retry delay (milliseconds). See + * also {@link AWSIotMqttClient#getMaxRetryDelay()}. + */ + public static final int CONNECTION_MAX_RETRY_DELAY = 30000; + + /** + * The default value for maximum offline queue size. See also + * {@link AWSIotMqttClient#getMaxOfflineQueueSize()}. + */ + public static final int MAX_OFFLINE_QUEUE_SIZE = 64; + + /** + * The default value for device reporting interval (milliseconds). See also + * {@link AWSIotDevice#getReportInterval()}. + */ + public static final int DEVICE_REPORT_INTERVAL = 3000; + + /** + * The default value for enabling device update versioning. See also + * {@link AWSIotDevice#isEnableVersioning()}. + */ + public static final boolean DEVICE_ENABLE_VERSIONING = false; + + /** + * The default value for device reporting QoS level. See also + * {@link AWSIotDevice#getDeviceReportQos()}. + */ + public static final int DEVICE_REPORT_QOS = 0; + + /** + * The default value for the QoS level for subscribing to shadow updates. + * See also {@link AWSIotDevice#getShadowUpdateQos()}. + */ + public static final int DEVICE_SHADOW_UPDATE_QOS = 0; + + /** + * The default value for the QoS level for publishing shadow methods. See + * also {@link AWSIotDevice#getMethodQos()}. + */ + public static final int DEVICE_METHOD_QOS = 0; + + /** + * The default value for the QoS level for subscribing to shadow method + * acknowledgement. See also {@link AWSIotDevice#getMethodAckQos()}. + */ + public static final int DEVICE_METHOD_ACK_QOS = 0; + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotConnectionStatus.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotConnectionStatus.java new file mode 100644 index 00000000000..1557af89c08 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotConnectionStatus.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client; + +/** + * Connection status that can be retrieved through + * {@link AWSIotMqttClient#getConnectionStatus()}. + */ +public enum AWSIotConnectionStatus { + + /** Client successfully connected. */ + CONNECTED, + + /** Not connected. */ + DISCONNECTED, + + /** Automatically reconnecting after connection loss. */ + RECONNECTING + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotDevice.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotDevice.java new file mode 100644 index 00000000000..90fa760cbf5 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotDevice.java @@ -0,0 +1,479 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client; + +import com.amazonaws.services.iot.client.shadow.AbstractAwsIotDevice; + +/** + * This class encapsulates all the functionalities that one can use to interact + * with AWS IoT device shadows in the cloud. For more information about AWS IoT + * device shadow, please refer to the AWS IoT developer guide. + *

+ * {@link AWSIotDevice} represents a device that is one-to-one mapped with the + * AWS IoT device shadow. The linkage is created through the shadow name that is + * passed into the constructor. + *

+ *

+ * There are two typical ways of using {@link AWSIotDevice}. One is to extend + * {@link AWSIotDevice} and provide device attributes that are to be + * synchronized with the shadow and their accessor methods (getters and + * setters). The other way is to use the get/update/delete methods provided in + * this class to directly access the shadow document. The first approach is easy + * to implement and should work for most of the use cases; the second approach + * provides the user the ability of directly accessing the data (document) + * stored on the device shadow, which is very flexible, however the user is + * responsible for parsing the shadow document encoded in JSON, and providing + * shadow-compatible document in update calls. It's also possible to use both + * approaches in a same application. + *

+ *

+ * To leverage the synchronization function provided by the library, one needs + * to extend {@link AWSIotDevice}. Device attributes that are to be kept in sync + * with the shadow must be annotated with {@link AWSIotDeviceProperty}. One + * should also provide getter functions for these annotated attributes to be + * reported to the shadow as well as setter functions to accept updates from the + * shadow. A simplified example is like this + *

+ * + *
+ *     public class SomeDevice extends AWSIotDevice {
+ *        {@literal @}AWSIotDeviceProperty
+ *         boolean switch;
+ *         
+ *         public boolean getSwitch() {
+ *              // read from the device and return the value to be reported to the shadow
+ *              return ...;
+ *         }
+ *         
+ *         public void setSwitch(boolean requestedState) {
+ *              // write to the device with the requested value from the shadow
+ *         }
+ *     }
+ * 
+ *

+ * To linked the above class with the shadow, one could do like so + *

+ * + *
+ *     AWSIotMqttClient client = new AWSIotMqttClient(...);
+ *     
+ *     SomeDevice someDevice = new SomeDevice(thingName);
+ *     
+ *     client.attach(someDevice);
+ *     
+ *     client.connect();
+ * 
+ *

+ * To access the shadow directly, one could do as the below. All the methods in + * this class are thread-safe, therefore can be called in different user + * threads. + *

+ * + *
+ *     AWSIotMqttClient client = new AWSIotMqttClient(...);
+ *     
+ *     AWSIotDevice awsIotDevice = new AWSIotDevice(thingName);
+ *     
+ *     client.attach(awsIotDevice);
+ *     
+ *     client.connect();
+ *     
+ *     ...
+ *     String jsonDocument = awsIotDevice.get();
+ *     ...
+ *     client.update(jsonDocument);
+ *     ...
+ * 
+ *

+ * The library contains sample applications that demonstrate how each of these + * two methods can be used. + *

+ */ +public class AWSIotDevice extends AbstractAwsIotDevice { + + /** + * Instantiates a new device instance. + * + * @param thingName + * the thing name + */ + public AWSIotDevice(String thingName) { + super(thingName); + } + + /** + * Gets the device report interval. + * + * @return the report interval in milliseconds. + */ + @Override + public long getReportInterval() { + return super.getReportInterval(); + } + + /** + * Sets the device report interval in milliseconds. This value must be set + * before the device is attached to a client via the + * {@link AWSIotMqttClient#attach(AWSIotDevice)} call. The default interval + * is 3,000ms. Setting it to 0 will disable reporting. + * + * @param reportInterval + * the new report interval + */ + @Override + public void setReportInterval(long reportInterval) { + super.setReportInterval(reportInterval); + } + + /** + * Checks if versioning is enabled for device updates. + * + * @return true, if versioning is enabled for device updates. + */ + @Override + public boolean isEnableVersioning() { + return super.isEnableVersioning(); + } + + /** + * Sets the device update versioning to be enabled or disabled. This value + * must be set before the device is attached to a client via the + * {@link AWSIotMqttClient#attach(AWSIotDevice)} call. + * + * @param enableVersioning + * true to enable device update versioning; false to disable. + */ + @Override + public void setEnableVersioning(boolean enableVersioning) { + super.setEnableVersioning(enableVersioning); + } + + /** + * Gets the MQTT QoS level for publishing the device report. The default QoS + * is QoS 0. + * + * @return the device report QoS + */ + @Override + public AWSIotQos getDeviceReportQos() { + return super.getDeviceReportQos(); + } + + /** + * Sets the MQTT QoS level for publishing the device report. This value must + * be set before the device is attached to a client via the + * {@link AWSIotMqttClient#attach(AWSIotDevice)} call. + * + * @param deviceReportQos + * the new device report QoS + */ + @Override + public void setDeviceReportQos(AWSIotQos deviceReportQos) { + super.setDeviceReportQos(deviceReportQos); + } + + /** + * Gets the MQTT QoS level for subscribing to shadow updates. The default + * QoS is QoS 0. + * + * @return the shadow update QoS + */ + @Override + public AWSIotQos getShadowUpdateQos() { + return super.getShadowUpdateQos(); + } + + /** + * Sets the MQTT QoS level for subscribing to shadow updates. This value + * must be set before the device is attached to a client via the + * {@link AWSIotMqttClient#attach(AWSIotDevice)} call. + * + * @param shadowUpdateQos + * the new shadow update QoS + */ + @Override + public void setShadowUpdateQos(AWSIotQos shadowUpdateQos) { + super.setShadowUpdateQos(shadowUpdateQos); + } + + /** + * Gets the MQTT QoS level for sending the shadow methods, namely Get, + * Update, and Delete. The default QoS is QoS 0. + * + * @return the QoS level for sending shadow methods. + */ + @Override + public AWSIotQos getMethodQos() { + return super.getMethodQos(); + } + + /** + * Sets the MQTT QoS level for sending shadow methods. This value must be + * set before the device is attached to a client via the + * {@link AWSIotMqttClient#attach(AWSIotDevice)} call. + * + * @param methodQos + * the new QoS level for sending shadow methods. + */ + @Override + public void setMethodQos(AWSIotQos methodQos) { + super.setMethodQos(methodQos); + } + + /** + * Gets the MQTT QoS level for subscribing to acknowledgement messages of + * shadow methods. The default QoS is QoS 0. + * + * @return the QoS level for subscribing to acknowledgement messages. + */ + @Override + public AWSIotQos getMethodAckQos() { + return super.getMethodAckQos(); + } + + /** + * Sets the MQTT QoS level for subscribing to acknowledgement messages of + * shadow methods. This value must be set before the device is attached to a + * client via the {@link AWSIotMqttClient#attach(AWSIotDevice)} call. + * + * @param methodAckQos + * the new QoS level for subscribing to acknowledgement messages. + */ + @Override + public void setMethodAckQos(AWSIotQos methodAckQos) { + super.setMethodAckQos(methodAckQos); + } + + /** + * Retrieves the latest state stored in the thing shadow. This method + * returns the full JSON document, including meta data. This is a blocking + * call, so the calling thread will be blocked until the operation succeeded + * or failed. + *

+ * Note: Blocking API call without specifying a timeout, in very rare cases, + * can block the calling thread indefinitely, if the server response is not + * received or lost. Use the alternative APIs with timeout for applications + * that expect responses within fixed duration. + *

+ * + * @return the JSON document of the device state + * @throws AWSIotException + * exception thrown if the operation fails + */ + @Override + public String get() throws AWSIotException { + return super.get(); + } + + /** + * Retrieves the latest state stored in the thing shadow. This method + * returns the full JSON document, including meta data. This is a blocking + * call, so the calling thread will be blocked until the operation + * succeeded, failed, or timed out. + * + * @param timeout + * the timeout in milliseconds that the calling thread will wait + * @return the JSON document of the device state + * @throws AWSIotException + * exception thrown if the operation fails + * @throws AWSIotTimeoutException + * exception thrown if the operation times out + */ + @Override + public String get(long timeout) throws AWSIotException, AWSIotTimeoutException { + return super.get(timeout); + } + + /** + * Retrieves the latest state stored in the thing shadow. This method + * returns the full JSON document, including meta data. This is a + * non-blocking call, so it immediately returns once is the operation has + * been queued in the system. The result of the operation will be notified + * through the callback functions, namely {@link AWSIotMessage#onSuccess}, + * {@link AWSIotMessage#onFailure}, and {@link AWSIotMessage#onTimeout}, one + * of which will be invoked after the operation succeeded, failed, or timed + * out respectively. + * + * @param message + * the message object contains callback functions; if the call is + * successful, the full JSON document of the device state will be + * stored in the {@code payload} field of {@code message}. + * @param timeout + * the timeout in milliseconds for the operation to be considered + * timed out + * @throws AWSIotException + * exception thrown if the operation fails + */ + @Override + public void get(AWSIotMessage message, long timeout) throws AWSIotException { + super.get(message, timeout); + } + + /** + * Updates the content of a thing shadow with the data provided in the + * request. This is a blocking call, so the calling thread will be blocked + * until the operation succeeded or failed. + *

+ * Note: Blocking API call without specifying a timeout, in very rare cases, + * can block the calling thread indefinitely, if the server response is not + * received or lost. Use the alternative APIs with timeout for applications + * that expect responses within fixed duration. + *

+ * + * @param jsonState + * the JSON document of the new device state + * @throws AWSIotException + * exception thrown if the operation fails + */ + @Override + public void update(String jsonState) throws AWSIotException { + super.update(jsonState); + } + + /** + * Updates the content of a thing shadow with the data provided in the + * request. This is a blocking call, so the calling thread will be blocked + * until the operation succeeded, failed, or timed out. + * + * @param jsonState + * the JSON document of the new device state + * @param timeout + * the timeout in milliseconds that the calling thread will wait + * @throws AWSIotException + * exception thrown if the operation fails + * @throws AWSIotTimeoutException + * exception thrown if the operation times out + */ + @Override + public void update(String jsonState, long timeout) throws AWSIotException, AWSIotTimeoutException { + super.update(jsonState, timeout); + } + + /** + * Updates the content of a thing shadow with the data provided in the + * request. This is a non-blocking call, so it immediately returns once is + * the operation has been queued in the system. The result of the operation + * will be notified through the callback functions, namely + * {@link AWSIotMessage#onSuccess}, {@link AWSIotMessage#onFailure}, and + * {@link AWSIotMessage#onTimeout}, one of which will be invoked after the + * operation succeeded, failed, or timed out respectively. + * + * @param message + * the message object contains callback functions + * @param timeout + * the timeout in milliseconds for the operation to be considered + * timed out + * @throws AWSIotException + * exception thrown if the operation fails + */ + @Override + public void update(AWSIotMessage message, long timeout) throws AWSIotException { + super.update(message, timeout); + } + + /** + * Deletes the content of a thing shadow. This is a blocking call, so the + * calling thread will be blocked until the operation succeeded or failed. + *

+ * Note: Blocking API call without specifying a timeout, in very rare cases, + * can block the calling thread indefinitely, if the server response is not + * received or lost. Use the alternative APIs with timeout for applications + * that expect responses within fixed duration. + *

+ * + * @throws AWSIotException + * exception thrown if the operation fails + */ + @Override + public void delete() throws AWSIotException { + super.delete(); + } + + /** + * Deletes the content of a thing shadow. This is a blocking call, so the + * calling thread will be blocked until the operation succeeded, failed, or + * timed out. + * + * @param timeout + * the timeout in milliseconds that the calling thread will wait + * @throws AWSIotException + * exception thrown if the operation fails + * @throws AWSIotTimeoutException + * exception thrown if the operation times out + */ + @Override + public void delete(long timeout) throws AWSIotException, AWSIotTimeoutException { + super.delete(timeout); + } + + /** + * Deletes the content of a thing shadow. This is a non-blocking call, so it + * immediately returns once is the operation has been queued in the system. + * The result of the operation will be notified through the callback + * functions, namely {@link AWSIotMessage#onSuccess}, + * {@link AWSIotMessage#onFailure}, and {@link AWSIotMessage#onTimeout}, one + * of which will be invoked after the operation succeeded, failed, or timed + * out respectively. + * + * @param message + * the message object contains callback functions + * @param timeout + * the timeout in milliseconds for the operation to be considered + * timed out + * @throws AWSIotException + * exception thrown if the operation fails + */ + @Override + public void delete(AWSIotMessage message, long timeout) throws AWSIotException { + super.delete(message, timeout); + } + + /** + * This function handles update messages received from the shadow. By + * default, it invokes the setter methods provided for the annotated device + * attributes. When there are multiple attribute changes received in one + * shadow update, the order of invoking the setter methods are not defined. + * One can override this function to provide their own implementation for + * updating the device. The shadow update containing the delta (between the + * 'desired' state and the 'reported' state) is passed in as an input + * argument. + * + * @param jsonState + * the JSON document containing the delta between 'desired' and + * 'reported' states + */ + @Override + public void onShadowUpdate(String jsonState) { + super.onShadowUpdate(jsonState); + } + + /** + * This function handles collecting device data for reporting to the shadow. + * By default, it invokes the getter methods provided for the annotated + * device attributes. The data is serialized in a JSON document and reported + * to the shadow. One could override this default implementation and provide + * their own JSON document for reporting. + * + * @return the JSON document containing 'reported' state + */ + @Override + public String onDeviceReport() { + return super.onDeviceReport(); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotDeviceErrorCode.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotDeviceErrorCode.java new file mode 100644 index 00000000000..fc638a3713e --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotDeviceErrorCode.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client; + +/** + * These error codes are used by the server in acknowledgement message for the + * shadow methods, namely Get, Update, and Delete. + */ +public enum AWSIotDeviceErrorCode { + + /** The bad request. */ + BAD_REQUEST(400), + /** The Unauthorized. */ + UNAUTHORIZED(401), + /** The Forbidden. */ + FORBIDDEN(403), + /** The Not found. */ + NOT_FOUND(404), + /** The Conflict. */ + CONFLICT(409), + /** The Payload too large. */ + PAYLOAD_TOO_LARGE(413), + /** The Unsupported media type. */ + UNSUPPORTED_MEDIA_TYPE(415), + /** The Too many requests. */ + TOO_MANY_REQUESTS(429), + /** The Internal service failure. */ + INTERNAL_SERVICE_FAILURE(429); + + /** The error code. */ + private final long errorCode; + + /** + * Instantiates a new device error code object. + * + * @param errorCode + * the error code + */ + private AWSIotDeviceErrorCode(final long errorCode) { + this.errorCode = errorCode; + } + + /** + * Gets the error code value. + * + * @return the error code value + */ + public long getValue() { + return this.errorCode; + } + + /** + * Returns the Enum representation of the error code value + * + * @param code + * the error code value + * @return the Enum representation of the error code, or null if the error + * code is unknown + */ + public static AWSIotDeviceErrorCode valueOf(long code) { + for (AWSIotDeviceErrorCode errorCode : AWSIotDeviceErrorCode.values()) { + if (errorCode.errorCode == code) { + return errorCode; + } + } + + return null; + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotDeviceProperty.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotDeviceProperty.java new file mode 100644 index 00000000000..c9abeaa593f --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotDeviceProperty.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Annotation class that is used to annotate properties in {@link AWSIotDevice}. + * Properties annotated must be accessible via corresponding getter and setter + * methods. + *

+ * With optional values provided by this annotation class, properties can also + * be configured to disable reporting to the shadow or not to accept updates + * from the shadow. If {@link #enableReport()} is disabled, property getter + * function is not required. Likewise, if {@link #allowUpdate()} is disabled for + * a property, its setter method is not required. + *

+ */ +@Retention(RetentionPolicy.RUNTIME) +public @interface AWSIotDeviceProperty { + + /** + * An optional name can be provided to the annotated property, which will be + * used for publishing to the shadow as well as receiving updates from the + * shadow. If not provided, the actual property name will be used. + * + * @return the name of the property. + */ + String name() default ""; + + /** + * Enable reporting the annotated property to the shadow. It's enabled by + * default. + * + * @return true to enable reporting to the shadow, false otherwise. + */ + boolean enableReport() default true; + + /** + * Allow updates from the shadow. It's enabled by default. + * + * @return true to allow updates from the shadow, false otherwise. + */ + boolean allowUpdate() default true; + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotException.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotException.java new file mode 100644 index 00000000000..c2af1ac0805 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotException.java @@ -0,0 +1,75 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client; + +import lombok.Getter; +import lombok.Setter; + +/** + * This is a generic exception that can be thrown in most of the APIs, blocking + * and non-blocking, by the library. + */ +public class AWSIotException extends Exception { + + /** The Constant serialVersionUID. */ + private static final long serialVersionUID = 1L; + + /** + * Error code for shadow methods. It's only applicable to exceptions thrown + * by those shadow method APIs. + * + * @param errorCode the new error code for the shadow method exception + * @return the error code of the shadow method exception + */ + @Getter + @Setter + private AWSIotDeviceErrorCode errorCode; + + /** + * Instantiates a new exception object. + * + * @param message + * the error message + */ + public AWSIotException(String message) { + super(message); + } + + /** + * Instantiates a new exception object. + * + * @param errorCode + * the error code + * @param message + * the error message + */ + public AWSIotException(AWSIotDeviceErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + /** + * Instantiates a new exception object. + * + * @param cause + * the cause. A null value is permitted, and indicates that the + * cause is nonexistent or unknown. + */ + public AWSIotException(Throwable cause) { + super(cause); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotMessage.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotMessage.java new file mode 100644 index 00000000000..63f55bff3bf --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotMessage.java @@ -0,0 +1,226 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client; + +import java.io.UnsupportedEncodingException; + +import com.amazonaws.services.iot.client.core.AwsIotMessageCallback; +import com.amazonaws.services.iot.client.core.AwsIotRuntimeException; + +import lombok.Getter; +import lombok.Setter; + +/** + * A common data structure that is used in a lot of non-blocking APIs in this + * library. + *

+ * It provides common data elements, such as {@link #topic}, {@link #qos}, and + * {@link #payload}, used by the APIs. + *

+ *

+ * It also contains callback functions that can be overridden to provide + * customized handlers. The callback functions are invoked when a non-blocking + * API call has completed successfully, unsuccessfully, or timed out. + * Applications wish to have customized callback functions must extend this + * class or its child classes, such as {@link AWSIotTopic}. + */ +public class AWSIotMessage implements AwsIotMessageCallback { + + /** + * The topic the message is received from or published to. + * + * @param topic the new topic of the message + * @return the current topic of the message + */ + @Getter + @Setter + protected String topic; + + /** + * The MQTT QoS level for the message. + * + * @param qos the new QoS level + * @return the current QoS level + */ + @Getter + @Setter + protected AWSIotQos qos; + + /** + * The payload of the message. + */ + protected byte[] payload; + + /** + * Error code for shadow methods. It's only applicable to messages returned + * by those shadow method APIs. + * + * @param errorCode the new error code for the shadow method + * @return the current error code of the shadow method + */ + @Getter + @Setter + protected AWSIotDeviceErrorCode errorCode; + + /** + * Error message for shadow methods. It's only applicable to messages + * returned by those shadow method APIs. + * + * @param errorMessage the new error message for the shadow method + * @return the current error message of the shadow method + */ + @Getter + @Setter + protected String errorMessage; + + /** + * Instantiates a new message object. + * + * @param topic + * the topic of the message + * @param qos + * the QoS level of the message + */ + public AWSIotMessage(String topic, AWSIotQos qos) { + this.topic = topic; + this.qos = qos; + } + + /** + * Instantiates a new message object. + * + * @param topic + * the topic of the message + * @param qos + * the QoS level of the message + * @param payload + * the payload of the message + */ + public AWSIotMessage(String topic, AWSIotQos qos, byte[] payload) { + this.topic = topic; + this.qos = qos; + setPayload(payload); + } + + /** + * Instantiates a new message object. + * + * @param topic + * the topic of the message + * @param qos + * the QoS level of the message + * @param payload + * the payload of the message + */ + public AWSIotMessage(String topic, AWSIotQos qos, String payload) { + this.topic = topic; + this.qos = qos; + setStringPayload(payload); + } + + /** + * Gets the byte array payload. + * + * @return the byte array payload + */ + public byte[] getPayload() { + if (payload == null) { + return null; + } + + return payload.clone(); + } + + /** + * Sets the byte array payload. + * + * @param payload + * the new byte array payload + */ + public void setPayload(byte[] payload) { + if (payload == null) { + this.payload = null; + return; + } + + this.payload = payload.clone(); + } + + /** + * Gets the string payload. + * + * @return the string payload + */ + public String getStringPayload() { + if (payload == null) { + return null; + } + + String str; + try { + str = new String(payload, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new AwsIotRuntimeException(e); + } + return str; + } + + /** + * Sets the string payload. + * + * @param payload + * the new string payload + */ + public void setStringPayload(String payload) { + if (payload == null) { + this.payload = null; + return; + } + + try { + this.payload = payload.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new AwsIotRuntimeException(e); + } + } + + /** + * Callback function to be invoked a non-block API has completed + * successfully. + */ + @Override + public void onSuccess() { + // Default callback implementation is no-op + } + + /** + * Callback function to be invoked a non-block API has completed + * unsuccessfully. + */ + @Override + public void onFailure() { + // Default callback implementation is no-op + } + + /** + * Callback function to be invoked a non-block API has timed out. + */ + @Override + public void onTimeout() { + // Default callback implementation is no-op + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotMqttClient.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotMqttClient.java new file mode 100644 index 00000000000..c9b5547743f --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotMqttClient.java @@ -0,0 +1,1060 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client; + +import java.security.KeyStore; + +import com.amazonaws.services.iot.client.core.AbstractAwsIotClient; + +/** + * This class is the main interface of the AWS IoT Java library. It provides + * both blocking and non-blocking methods for interacting with AWS IoT services + * over the MQTT protocol. With this client, one can directly publish messages + * to the AWS IoT service and subscribe or unsubscribe to any pub/sub topics. + * When using this class in conjunction with {@link AWSIotDevice}, one can + * easily access AWS IoT device shadows in the cloud, and keep them in sync with + * the real devices. + *

+ * There are two types of connections this SDK supports to connect to the AWS + * IoT service: + *

+ *
    + *
  • MQTT (over TLS 1.2) with X.509 certificate based mutual authentication
  • + *
  • MQTT over Secure WebSocket with AWS SigV4 authentication
  • + *
+ *

+ * For MQTT over TLS, a {@link KeyStore} containing a valid device certificate + * and private key is required for instantiating the client. Password for + * decrypting the private key in the KeyStore must also be provided. + *

+ *

+ * For MQTT over WebSocket, AWS Signature Version 4 (SigV4) protocol is used for + * device authentication. For that, a valid AWS IAM access Id and access key + * pair is required for instantiating the client. + *

+ * In both cases, AWS IoT IAM policies must be configured properly before the + * connection can be established with the AWS IoT Gateway. For more information + * about AWS IoT service, please refer to the AWS IoT developer guide. + *

+ *

+ * To use the client directly, a typical flow would be like the below, and since + * methods in this class are thread-safe, publish and subscribe can be called + * from different threads. + *

+ * + *
+ * {@code
+ *     AWSIotMqttClient client = new AWSIotMqttClient(...);
+ *     
+ *     client.connect();
+ *     
+ *     ...
+ *     client.subscribe(topic, ...)
+ *     ...
+ *     client.publish(message, ...)
+ * }
+ * 
+ *

+ * When using this client in conjunction with {@link AWSIotDevice}, one can + * implement a device that is always synchronized with its AWS IoT shadow by + * just providing getter and setter methods for the device attributes. The + * library does all the heavy lifting by collecting device attributes using the + * getter methods provided and reporting to the shadow periodically. It also + * subscribes to device changes and updates the device by calling provided + * setter methods whenever a change is received. All of these are handled by the + * library with no extra code required from the user. {@link AWSIotDevice} also + * provides methods for accessing device shadows directly. Please refer to + * {@link AWSIotDevice} for more details. A typical flow would be like below. + *

+ * + *
+ * {@code
+ *     AWSIotMqttClient client = new AWSIotMqttClient(...);
+ *     
+ *     SomeDevice someDevice = new SomeDevice(thingName);    // SomeDevice extends AWSIotDevice
+ *     
+ *     client.attach(someDevice);
+ *     
+ *     client.connect();
+ * }
+ * 
+ *

+ * The library contains sample applications that demonstrate different ways of + * using this client library. + *

+ */ +public class AWSIotMqttClient extends AbstractAwsIotClient { + + /** + * Instantiates a new client using TLS 1.2 mutual authentication. Client + * certificate and private key are passed in through the {@link KeyStore} + * argument. The key password protecting the private key in the + * {@link KeyStore} is also required. + * + * @param clientEndpoint + * the client endpoint in the form of {@code .iot..amazonaws.com}. The account-specific + * prefix can be found on the AWS IoT console or by using the + * {@code describe-endpoint} command through the AWS command line + * interface. + * @param clientId + * the client ID uniquely identify a MQTT connection. Two clients + * with the same client ID are not allowed to be connected + * concurrently to a same endpoint. + * @param keyStore + * the key store containing the client X.509 certificate and + * private key. The {@link KeyStore} object can be constructed + * using X.509 certificate file and private key file created on + * the AWS IoT console. For more details, please refer to the + * README file of this SDK. + * @param keyPassword + * the key password protecting the private key in the + * {@code keyStore} argument. + */ + public AWSIotMqttClient(String clientEndpoint, String clientId, KeyStore keyStore, String keyPassword) { + super(clientEndpoint, clientId, keyStore, keyPassword); + } + + /** + * Instantiates a new client using Secure WebSocket and AWS SigV4 + * authentication. AWS IAM credentials, including the access key ID and + * secret access key, are required for signing the request. Credentials can + * be permanent ones associated with IAM users or temporary ones generated + * via the AWS Cognito service. + * + * @param clientEndpoint + * the client endpoint in the form of + * {@literal .iot..amazonaws.com} + * . The account-specific prefix can be found on the AWS IoT + * console or by using the {@code describe-endpoint} command + * through the AWS command line interface. + * @param clientId + * the client ID uniquely identify a MQTT connection. Two clients + * with the same client ID are not allowed to be connected + * concurrently to a same endpoint. + * @param awsAccessKeyId + * the AWS access key id + * @param awsSecretAccessKey + * the AWS secret access key + */ + public AWSIotMqttClient(String clientEndpoint, String clientId, String awsAccessKeyId, String awsSecretAccessKey) { + super(clientEndpoint, clientId, awsAccessKeyId, awsSecretAccessKey, null); + } + + /** + * Instantiates a new client using Secure WebSocket and AWS SigV4 + * authentication. AWS IAM credentials, including the access key ID and + * secret access key, are required for signing the request. Credentials can + * be permanent ones associated with IAM users or temporary ones generated + * via the AWS Cognito service. + * + * @param clientEndpoint + * the client endpoint in the form of + * {@literal .iot..amazonaws.com} + * . The account-specific prefix can be found on the AWS IoT + * console or by using the {@code describe-endpoint} command + * through the AWS command line interface. + * @param clientId + * the client ID uniquely identify a MQTT connection. Two clients + * with the same client ID are not allowed to be connected + * concurrently to a same endpoint. + * @param awsAccessKeyId + * the AWS access key id + * @param awsSecretAccessKey + * the AWS secret access key + * @param sessionToken + * Session token received along with the temporary credentials + * from services like STS server, AssumeRole, or Amazon Cognito. + */ + public AWSIotMqttClient(String clientEndpoint, String clientId, String awsAccessKeyId, String awsSecretAccessKey, + String sessionToken) { + super(clientEndpoint, clientId, awsAccessKeyId, awsSecretAccessKey, sessionToken); + } + + /** + * Gets the number of client threads currently configured. Each client has + * their own thread pool, which is used to execute user callback functions + * as well as any timeout callback functions requested. By default, the + * thread pool is configured with one execution thread. + * + * @return the number of client threads + */ + @Override + public int getNumOfClientThreads() { + return super.getNumOfClientThreads(); + } + + /** + * Sets a new value for the number of client threads. This value must be set + * before {@link #connect()} is called. + * + * @param numOfClientThreads + * the new number of client threads. The default value is 1. + */ + @Override + public void setNumOfClientThreads(int numOfClientThreads) { + super.setNumOfClientThreads(numOfClientThreads); + } + + /** + * Gets the connection timeout in milliseconds currently configured. + * Connection timeout specifies how long the client should wait for the + * connection to be established with the server. By default, it's 30,000ms. + * + * @return the connection timeout + */ + @Override + public int getConnectionTimeout() { + return super.getConnectionTimeout(); + } + + /** + * Sets a new value in milliseconds for the connection timeout. This value + * must be set before {@link #connect()} is called. + * + * @param connectionTimeout + * the new connection timeout. The default value is 30,000ms. + */ + @Override + public void setConnectionTimeout(int connectionTimeout) { + super.setConnectionTimeout(connectionTimeout); + } + + /** + * Gets the maximum number of connection retries currently configured. + * Connections will be automatically retried for the configured maximum + * times when failing to be established or lost. User disconnect, requested + * via {@link #disconnect()} will not be retried. By default, it's 5 times. + * Setting it to 0 will disable the connection retry function. + * + * @return the max connection retries + */ + @Override + public int getMaxConnectionRetries() { + return super.getMaxConnectionRetries(); + } + + /** + * Sets a new value for the maximum connection retries. This value must be + * set before {@link #connect()} is called. Setting it to 0 will disable the + * connection retry function. + * + * @param maxConnectionRetries + * the new max connection retries. The default value is 5. + */ + @Override + public void setMaxConnectionRetries(int maxConnectionRetries) { + super.setMaxConnectionRetries(maxConnectionRetries); + } + + /** + * Gets the base retry delay in milliseconds currently configured. For each + * connection failure, a brief delay has to elapse before the connection is + * retried. The retry delay is calculated using this simple formula + * {@code delay = min(baseRetryDelay * pow(2, numRetries), maxRetryDelay)}. + * By default, the base retry delay is 3,000ms. + * + * @return the base retry delay + */ + @Override + public int getBaseRetryDelay() { + return super.getBaseRetryDelay(); + } + + /** + * Sets a new value in milliseconds for the base retry delay. This value + * must be set before {@link #connect()} is called. + * + * @param baseRetryDelay + * the new base retry delay. The default value is 3,000ms. + */ + @Override + public void setBaseRetryDelay(int baseRetryDelay) { + super.setBaseRetryDelay(baseRetryDelay); + } + + /** + * Gets the maximum retry delay in milliseconds currently configured. For + * each connection failure, a brief delay has to elapse before the + * connection is retried. The retry delay is calculated using this simple + * formula + * {@code delay = min(baseRetryDelay * pow(2, numRetries), maxRetryDelay)}. + * By default, the maximum retry delay is 30,000ms. + * + * @return the maximum retry delay + */ + @Override + public int getMaxRetryDelay() { + return super.getMaxRetryDelay(); + } + + /** + * Sets a new value in milliseconds for the maximum retry delay. This value + * must be set before {@link #connect()} is called. + * + * @param maxRetryDelay + * the new max retry delay. The default value is 30,000ms. + */ + @Override + public void setMaxRetryDelay(int maxRetryDelay) { + super.setMaxRetryDelay(maxRetryDelay); + } + + /** + * Gets the server acknowledge timeout in milliseconds currently configured. + * This timeout is used internally by the SDK when subscribing to shadow + * confirmation topics for get, update, and delete requests. It's also used + * for re-subscribing to user topics when the connection is retried. For + * most of the APIs provided in the SDK, the user can specify the timeout as + * an argument. By default, the server acknowledge timeout is 3,000ms. + * + * @return the server acknowledge timeout + */ + @Override + public int getServerAckTimeout() { + return super.getServerAckTimeout(); + } + + /** + * Sets a new value in milliseconds for the default server acknowledge + * timeout. This value must be set before {@link #connect()} is called. + * + * @param serverAckTimeout + * the new server acknowledge timeout. The default value is + * 3,000ms. + */ + @Override + public void setServerAckTimeout(int serverAckTimeout) { + super.setServerAckTimeout(serverAckTimeout); + } + + /** + * Gets the keep-alive interval for the MQTT connection in milliseconds + * currently configured. Setting this value to 0 will disable the keep-alive + * function for the connection. The default keep alive interval is 30,000ms. + * + * @return the keep alive interval + */ + @Override + public int getKeepAliveInterval() { + return super.getKeepAliveInterval(); + } + + /** + * Sets a new value in milliseconds for the connection keep-alive interval. + * This value must be set before {@link #connect()} is called. Setting this + * value to 0 will disable the keep-alive function. + * + * @param keepAliveInterval + * the new keep alive interval. The default value is 30,000ms. + */ + @Override + public void setKeepAliveInterval(int keepAliveInterval) { + super.setKeepAliveInterval(keepAliveInterval); + } + + /** + * Gets the maximum offline queue size current configured. The offline + * queues are used for temporarily holding outgoing requests while the + * connection is being established or retried. When the connection is + * established, offline queue messages will be sent out as usual. They can + * be useful for dealing with transient connection failures by allowing the + * application to continuously send requests while the connection is being + * established. Each type of request, namely publish, subscribe, and + * unsubscribe, has their own offline queue. The default offline queue size + * is 64. Setting it to 0 will disable the offline queues. + * + * @return the max offline queue size + */ + @Override + public int getMaxOfflineQueueSize() { + return super.getMaxOfflineQueueSize(); + } + + /** + * Sets a new value for the maximum offline queue size. This value must be + * set before {@link #connect()} is called. Setting it to 0 will disable the + * offline queues. + * + * @param maxOfflineQueueSize + * the new maximum offline queue size. The default value is 64. + */ + @Override + public void setMaxOfflineQueueSize(int maxOfflineQueueSize) { + super.setMaxOfflineQueueSize(maxOfflineQueueSize); + } + + /** + * Gets the Last Will and Testament message currently configured. The Last + * Will and Testament message with configured payload will be published when + * the client connection is lost or terminated ungracefully, i.e. not + * through {@link #disconnect()}. + * + * @return the will message + */ + @Override + public AWSIotMessage getWillMessage() { + return super.getWillMessage(); + } + + /** + * Sets a new Last Will and Testament message. The message must be set + * before {@link #connect()} is called. By default, Last Will and Testament + * message is not sent. + * + * @param willMessage + * the new Last Will and Testament message message. The default + * value is {@code null}. + */ + @Override + public void setWillMessage(AWSIotMessage willMessage) { + super.setWillMessage(willMessage); + } + + /** + * Connect the client to the server. This is a blocking call, so the calling + * thread will be blocked until the operation succeeded or failed. + *

+ * Note: Blocking API call without specifying a timeout, in very rare cases, + * can block the calling thread indefinitely, if the server response is not + * received or lost. Use the alternative APIs with timeout for applications + * that expect responses within fixed duration. + *

+ * + * @throws AWSIotException + * exception thrown if the connection operation fails + */ + @Override + public void connect() throws AWSIotException { + super.connect(); + } + + /** + * Connect the client to the server. This is a blocking call, so the calling + * thread will be blocked until the operation succeeded, failed, or timed + * out. + * + * @param timeout + * the timeout in milliseconds that the calling thread will wait + * @throws AWSIotException + * exception thrown if the operation fails + * @throws AWSIotTimeoutException + * exception thrown if the operation times out + */ + @Override + public void connect(long timeout) throws AWSIotException, AWSIotTimeoutException { + super.connect(timeout); + } + + /** + * Connect the client to the server. This call can be either blocking or + * non-blocking specified by the {@code blocking} argument. For blocking + * calls, the calling thread is blocked until the operation completed, + * failed, or timed out; for non-blocking calls, the calling thread will not + * be blocked while the connection is being established. + * + * @param timeout + * the timeout in milliseconds that the calling thread will wait + * @param blocking + * whether the call should be blocking or non-blocking + * @throws AWSIotException + * exception thrown if the operation fails + * @throws AWSIotTimeoutException + * exception thrown if the operation times out + */ + @Override + public void connect(long timeout, boolean blocking) throws AWSIotException, AWSIotTimeoutException { + super.connect(timeout, blocking); + } + + /** + * Disconnect the client from the server. This is a blocking call, so the + * calling thread will be blocked until the operation succeeded or failed. + *

+ * Note: Blocking API call without specifying a timeout, in very rare cases, + * can block the calling thread indefinitely, if the server response is not + * received or lost. Use the alternative APIs with timeout for applications + * that expect responses within fixed duration. + *

+ * + * @throws AWSIotException + * exception thrown if the operation fails + */ + @Override + public void disconnect() throws AWSIotException { + super.disconnect(); + } + + /** + * Disconnect the client from the server. This is a blocking call, so the + * calling thread will be blocked until the operation succeeded, failed, or + * timed out. + * + * @param timeout + * the timeout in milliseconds that the calling thread will wait + * @throws AWSIotException + * exception thrown if the operation fails + * @throws AWSIotTimeoutException + * exception thrown if the operation times out + */ + @Override + public void disconnect(long timeout) throws AWSIotException, AWSIotTimeoutException { + super.disconnect(timeout); + } + + /** + * Disconnect the client from the server. This call can be either blocking + * or non-blocking specified by the {@code blocking} argument. For blocking + * calls, the calling thread is blocked until the operation completed, + * failed, or timed out; for non-blocking calls, the calling thread will not + * be blocked while the connection is being terminated. + * + * @param timeout + * the timeout in milliseconds that the calling thread will wait + * @param blocking + * whether the call should be blocking or non-blocking + * @throws AWSIotException + * exception thrown if the operation fails + * @throws AWSIotTimeoutException + * exception thrown if the operation times out + */ + @Override + public void disconnect(long timeout, boolean blocking) throws AWSIotException, AWSIotTimeoutException { + super.disconnect(timeout, blocking); + } + + /** + * Publishes the payload to a given topic. This is a blocking call so the + * calling thread is blocked until the publish operation succeeded or + * failed. MQTT QoS0 is used for publishing the payload. + *

+ * Note: Blocking API call without specifying a timeout, in very rare cases, + * can block the calling thread indefinitely, if the server response is not + * received or lost. Use the alternative APIs with timeout for applications + * that expect responses within fixed duration. + *

+ * + * @param topic + * the topic to be published to + * @param payload + * the payload to be published + * @throws AWSIotException + * exception thrown if the publish operation fails + */ + @Override + public void publish(String topic, String payload) throws AWSIotException { + super.publish(topic, payload); + } + + /** + * Publishes the payload to a given topic. This is a blocking call so the + * calling thread is blocked until the publish operation succeeded, failed, + * or the specified timeout has elapsed. MQTT QoS0 is used for publishing + * the payload. + * + * @param topic + * the topic to be published to + * @param payload + * the payload to be published + * @param timeout + * the timeout in milliseconds that the calling thread will wait + * @throws AWSIotException + * the exception thrown if the publish operation fails + * @throws AWSIotTimeoutException + * the exception thrown if the publish operation times out + */ + @Override + public void publish(String topic, String payload, long timeout) throws AWSIotException, AWSIotTimeoutException { + super.publish(topic, payload, timeout); + } + + /** + * Publishes the payload to a given topic. This is a blocking call so the + * calling thread is blocked until the publish operation succeeded or + * failed. + *

+ * Note: Blocking API call without specifying a timeout, in very rare cases, + * can block the calling thread indefinitely, if the server response is not + * received or lost. Use the alternative APIs with timeout for applications + * that expect responses within fixed duration. + *

+ * + * @param topic + * the topic to be published to + * @param qos + * the MQTT QoS used for publishing + * @param payload + * the payload to be published + * @throws AWSIotException + * the exception thrown if the publish operation fails + */ + @Override + public void publish(String topic, AWSIotQos qos, String payload) throws AWSIotException { + super.publish(topic, qos, payload); + } + + /** + * Publishes the payload to a given topic. This is a blocking call so the + * calling thread is blocked until the publish operation succeeded, failed, + * or the specified timeout has elapsed. + * + * @param topic + * the topic to be published to + * @param qos + * the MQTT QoS used for publishing + * @param payload + * the payload to be published + * @param timeout + * the timeout in milliseconds that the calling thread will wait + * @throws AWSIotException + * the exception thrown if the publish operation fails + * @throws AWSIotTimeoutException + * the exception thrown if the publish operation times out + */ + @Override + public void publish(String topic, AWSIotQos qos, String payload, long timeout) throws AWSIotException, + AWSIotTimeoutException { + super.publish(topic, qos, payload, timeout); + } + + /** + * Publishes the raw payload to a given topic. This is a blocking call so + * the calling thread is blocked until the publish operation succeeded or + * failed. MQTT QoS0 is used for publishing the payload. + *

+ * Note: Blocking API call without specifying a timeout, in very rare cases, + * can block the calling thread indefinitely, if the server response is not + * received or lost. Use the alternative APIs with timeout for applications + * that expect responses within fixed duration. + *

+ * + * @param topic + * the topic to be published to + * @param payload + * the payload to be published + * @throws AWSIotException + * the exception thrown if the publish operation fails + */ + @Override + public void publish(String topic, byte[] payload) throws AWSIotException { + super.publish(topic, payload); + } + + /** + * Publishes the raw payload to a given topic. This is a blocking call so + * the calling thread is blocked until the publish operation succeeded, + * failed, or the specified timeout has elapsed. MQTT QoS0 is used for + * publishing the payload. + * + * @param topic + * the topic to be published to + * @param payload + * the payload to be published + * @param timeout + * the timeout in milliseconds that the calling thread will wait + * @throws AWSIotException + * the exception thrown if the publish operation fails + * @throws AWSIotTimeoutException + * the exception thrown if the publish operation times out + */ + @Override + public void publish(String topic, byte[] payload, long timeout) throws AWSIotException, AWSIotTimeoutException { + super.publish(topic, payload, timeout); + } + + /** + * Publishes the raw payload to a given topic. This is a blocking call so + * the calling thread is blocked until the publish operation is succeeded or + * failed. + *

+ * Note: Blocking API call without specifying a timeout, in very rare cases, + * can block the calling thread indefinitely, if the server response is not + * received or lost. Use the alternative APIs with timeout for applications + * that expect responses within fixed duration. + *

+ * + * @param topic + * the topic to be published to + * @param qos + * the MQTT QoS used for publishing + * @param payload + * the payload to be published + * @throws AWSIotException + * the exception thrown if the publish operation fails + */ + @Override + public void publish(String topic, AWSIotQos qos, byte[] payload) throws AWSIotException { + super.publish(topic, qos, payload); + } + + /** + * Publishes the raw payload to a given topic. This is a blocking call so + * the calling thread is blocked until the publish operation is succeeded, + * failed, or the specified timeout has elapsed. + * + * @param topic + * the topic to be published to + * @param qos + * the MQTT QoS used for publishing + * @param payload + * the payload to be published + * @param timeout + * the timeout in milliseconds that the calling thread will wait + * @throws AWSIotException + * the exception thrown if the publish operation fails + * @throws AWSIotTimeoutException + * the exception thrown if the publish operation times out + */ + @Override + public void publish(String topic, AWSIotQos qos, byte[] payload, long timeout) throws AWSIotException, + AWSIotTimeoutException { + super.publish(topic, qos, payload, timeout); + } + + /** + * Publishes the payload to a given topic. Topic, MQTT QoS, and payload are + * given in the {@code message} argument. This is a non-blocking call so it + * immediately returns once the operation has been queued in the system. The + * result of the operation will be notified through the callback functions, + * namely {@link AWSIotMessage#onSuccess} and + * {@link AWSIotMessage#onFailure}, one of which will be invoked after the + * operation succeeded or failed respectively. The default implementation + * for the callback functions in {@link AWSIotMessage} does nothing. The + * user could override one or more of these functions through subclassing. + * + * @param message + * the message, including the topic, MQTT QoS, and payload, to be + * published + * @throws AWSIotException + * the exception thrown if the publish operation fails to be + * queued + */ + @Override + public void publish(AWSIotMessage message) throws AWSIotException { + super.publish(message); + } + + /** + * Publishes the payload to a given topic. Topic, MQTT QoS, and payload are + * given in the {@code message} argument. This is a non-blocking call so it + * immediately returns once the operation has been queued in the system. The + * result of the operation will be notified through the callback functions, + * namely {@link AWSIotMessage#onSuccess}, {@link AWSIotMessage#onFailure}, + * and {@link AWSIotMessage#onTimeout}, one of which will be invoked after + * the operation succeeded, failed, or timed out respectively. The user + * could override one or more of these functions through subclassing. + * + * @param message + * the message, including the topic, MQTT QoS, and payload, to be + * published + * @param timeout + * the timeout in milliseconds for the operation to be considered + * timed out + * @throws AWSIotException + * the exception thrown if the publish operation fails to be + * queued + */ + @Override + public void publish(AWSIotMessage message, long timeout) throws AWSIotException { + super.publish(message, timeout); + } + + /** + * Subscribes to a given topic. Topic and MQTT QoS are given in the + * {@code topic} argument. This call can be either blocking or non-blocking + * specified by the {@code blocking} argument. For blocking calls, the + * calling thread is blocked until the subscribe operation completed or + * failed; for non-blocking calls, the result of the operation will be + * notified through the callback functions, namely + * {@link AWSIotTopic#onSuccess} and {@link AWSIotTopic#onFailure}, one of + * which will be invoked after the operation succeeded or failed + * respectively. For both blocking and non-blocking calls, callback function + * {@link AWSIotTopic#onMessage} is invoked when subscribed message arrives. + * The default implementation for the callback functions in + * {@link AWSIotTopic} does nothing. The user could override one or more of + * these functions through subclassing. + *

+ * Note: Blocking API call without specifying a timeout, in very rare cases, + * can block the calling thread indefinitely, if the server response is not + * received or lost. Use the alternative APIs with timeout for applications + * that expect responses within fixed duration. + *

+ * + * @param topic + * the topic to subscribe to + * @param blocking + * whether the call should be blocking or non-blocking + * @throws AWSIotException + * the exception thrown if the subscribe operation fails + * (blocking) or fails to be queued (non-blocking) + */ + @Override + public void subscribe(AWSIotTopic topic, boolean blocking) throws AWSIotException { + super.subscribe(topic, blocking); + } + + /** + * Subscribes to a given topic. Topic and MQTT QoS are given in the + * {@code topic} argument. This call can be either blocking or non-blocking + * specified by the {@code blocking} argument. For blocking call, the + * calling thread is blocked until the subscribe operation completed, + * failed, or timed out; for non-blocking call, the result of the operation + * will be notified through the callback functions, namely + * {@link AWSIotTopic#onSuccess}, {@link AWSIotTopic#onFailure} and + * {@link AWSIotTopic#onTimeout}, one of which will be invoked after the + * operation succeeded, failed, or timed out respectively. For both blocking + * and non-blocking calls, callback function {@link AWSIotTopic#onMessage} + * is invoked when subscribed message arrives. The default implementation + * for the callback functions in {@link AWSIotTopic} does nothing. The user + * could override one or more of these functions through subclassing. + * + * @param topic + * the topic to subscribe to + * @param timeout + * the timeout in milliseconds for the operation to be considered + * timed out + * @param blocking + * whether the call should be blocking or non-blocking + * @throws AWSIotException + * the exception thrown if the subscribe operation fails + * (blocking) or fails to be queued (non-blocking) + * @throws AWSIotTimeoutException + * the exception thrown if the subscribe operation times out. + * This exception is not thrown if the call is non-blocking; + * {@link AWSIotTopic#onTimeout} will be invoked instead if + * timeout happens. + */ + @Override + public void subscribe(AWSIotTopic topic, long timeout, boolean blocking) throws AWSIotException, + AWSIotTimeoutException { + super.subscribe(topic, timeout, blocking); + } + + /** + * Subscribes to a given topic. Topic and MQTT QoS are given in the + * {@code topic} argument. This is a non-blocking call so it immediately + * returns once is the operation has been queued in the system. The result + * of the operation will be notified through the callback functions, namely + * {@link AWSIotTopic#onSuccess} and {@link AWSIotTopic#onFailure}, one of + * which will be invoked after the operation succeeded or failed + * respectively. Another callback function, {@link AWSIotTopic#onMessage}, + * is invoked when subscribed message arrives. The default implementation + * for the callback functions in {@link AWSIotTopic} does nothing. The user + * could override one or more of these functions through sub-classing. + * + * @param topic + * the topic to subscribe to + * @throws AWSIotException + * the exception thrown if the subscribe operation fails to be + * queued + */ + @Override + public void subscribe(AWSIotTopic topic) throws AWSIotException { + super.subscribe(topic); + } + + /** + * Subscribes to a given topic. Topic and MQTT QoS are given in the + * {@code topic} argument. This is a non-blocking call so it immediately + * returns once is the operation has been queued in the system. The result + * of the operation will be notified through the callback functions, namely + * {@link AWSIotTopic#onSuccess}, {@link AWSIotTopic#onFailure}, and + * {@link AWSIotTopic#onTimeout}, one of which will be invoked after the + * operation succeeded, failed, or timed out respectively. Another callback + * function, {@link AWSIotTopic#onMessage}, is invoked when subscribed + * message arrives. The default implementation for the callback functions in + * {@link AWSIotTopic} does nothing. The user could override one or more of + * these functions through sub-classing. + * + * @param topic + * the topic to subscribe to + * @param timeout + * the timeout in milliseconds for the operation to be considered + * timed out + * @throws AWSIotException + * the exception thrown if the subscribe operation fails to be + * queued + */ + @Override + public void subscribe(AWSIotTopic topic, long timeout) throws AWSIotException { + super.subscribe(topic, timeout); + } + + /** + * Unsubscribes to a given topic. This is a blocking call, so the calling + * thread is blocked until the unsubscribe operation completed or failed. + *

+ * Note: Blocking API call without specifying a timeout, in very rare cases, + * can block the calling thread indefinitely, if the server response is not + * received or lost. Use the alternative APIs with timeout for applications + * that expect responses within fixed duration. + *

+ * + * @param topic + * the topic to unsubscribe to + * @throws AWSIotException + * the exception thrown if the unsubscribe operation fails + */ + @Override + public void unsubscribe(String topic) throws AWSIotException { + super.unsubscribe(topic); + } + + /** + * Unsubscribes to a given topic. This is a blocking call, so the calling + * thread is blocked until the unsubscribe operation completed, failed, or + * the specified timeout has elapsed. + * + * @param topic + * the topic to unsubscribe to + * @param timeout + * the timeout in milliseconds that the calling thread will wait + * @throws AWSIotException + * the exception thrown if the unsubscribe operation fails + * @throws AWSIotTimeoutException + * the exception thrown if the unsubscribe operation times out + */ + @Override + public void unsubscribe(String topic, long timeout) throws AWSIotException, AWSIotTimeoutException { + super.unsubscribe(topic, timeout); + } + + /** + * Unsubscribes to a given topic. This is a non-blocking call so it + * immediately returns once the operation has been queued in the system. The + * result of the operation will be notified through the callback functions, + * namely {@link AWSIotTopic#onSuccess} and {@link AWSIotTopic#onFailure}, + * one of which will be invoked after the operation succeeded or failed + * respectively. The default implementation for the callback functions in + * {@link AWSIotTopic} does nothing. The user could override one or more of + * these functions through subclassing. + * + * @param topic + * the topic to unsubscribe to + * @throws AWSIotException + * the exception thrown if the unsubscribe operation fails to be + * queued + */ + @Override + public void unsubscribe(AWSIotTopic topic) throws AWSIotException { + super.unsubscribe(topic); + } + + /** + * Unsubscribes to a given topic. This is a non-blocking call so it + * immediately returns once the operation has been queued in the system. The + * result of the operation will be notified through the callback functions, + * namely {@link AWSIotTopic#onSuccess}, {@link AWSIotTopic#onFailure}, and + * {@link AWSIotTopic#onTimeout}, one of which will be invoked after the + * operation succeeded, failed, or timed out respectively. The default + * implementation for the callback functions in {@link AWSIotTopic} does + * nothing. The user could override one or more of these functions through + * subclassing. + * + * @param topic + * the topic to unsubscribe to + * @param timeout + * the timeout in milliseconds for the operation to be considered + * timed out + * @throws AWSIotException + * the exception thrown if the unsubscribe operation fails to be + * queued + */ + @Override + public void unsubscribe(AWSIotTopic topic, long timeout) throws AWSIotException { + super.unsubscribe(topic, timeout); + } + + /** + * Attach a shadow device to the client. Once attached, the device, if + * configured, will be automatically synchronized with the AWS Thing shadow + * using this client and its connection. For more details about how to + * configure and use a device, please refer to {@link AWSIotDevice}. + * + * @param device + * the device to be attached to the client + * @throws AWSIotException + * the exception thrown if the attach operation fails + */ + @Override + public void attach(AWSIotDevice device) throws AWSIotException { + super.attach(device); + } + + /** + * Detach the given device from the client. Device and shadow + * synchronization will be stopped after the device is detached from the + * client. + * + * @param device + * the device to be detached from the client + * @throws AWSIotException + * the exception thrown if the detach operation fails + */ + @Override + public void detach(AWSIotDevice device) throws AWSIotException { + super.detach(device); + } + + /** + * Gets the connection status of the connection used by the client. + * + * @return the connection status + */ + @Override + public AWSIotConnectionStatus getConnectionStatus() { + return super.getConnectionStatus(); + } + + /** + * This callback function is called when the connection used by the client + * is successfully established. The user could supply a different callback + * function via subclassing, however the default implementation should + * always be called in the override function in order for the connection + * retry as well as device synchronization to work properly. + */ + @Override + public void onConnectionSuccess() { + super.onConnectionSuccess(); + } + + /** + * This callback function is called when the connection used by the client + * is temporarily lost. The user could supply a different callback function + * via subclassing, however the default implementation should always be + * called in the override function in order for the connection retry as well + * as device synchronization to work properly. + */ + @Override + public void onConnectionFailure() { + super.onConnectionFailure(); + } + + /** + * This callback function is called when the connection used by the client + * is permanently closed. The user could supply a different callback + * function via subclassing, however the default implementation should + * always be called in the override function in order for the connection + * retry as well as device synchronization to work properly. + */ + @Override + public void onConnectionClosed() { + super.onConnectionClosed(); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotQos.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotQos.java new file mode 100644 index 00000000000..fdf24dfba43 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotQos.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client; + +/** + * QoS definitions. The AWS IoT service supports QoS0 and QoS1 defined by the + * MQTT protocol. + */ +public enum AWSIotQos { + + /** The QoS0. */ + QOS0(0), + + /** The QoS1. */ + QOS1(1); + + /** The qos. */ + private final int qos; + + /** + * Instantiates a QoS object. + * + * @param qos + * the QoS level + */ + private AWSIotQos(final int qos) { + this.qos = qos; + } + + /** + * Gets the integer representation of the QoS + * + * @return the integer value of the QoS + */ + public int getValue() { + return this.qos; + } + + /** + * Gets the Enum representation of the QoS + * + * @param qos + * the integer value of the QoS + * @return the Enum value of the QoS + */ + public static AWSIotQos valueOf(int qos) { + if (qos == 0) { + return QOS0; + } else if (qos == 1) { + return QOS1; + } else { + throw new IllegalArgumentException("QoS not supported"); + } + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotTimeoutException.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotTimeoutException.java new file mode 100644 index 00000000000..ccd1805eb6f --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotTimeoutException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client; + +/** + * This timeout exception can be thrown by the blocking APIs in this library + * when expected time has elapsed. + */ +public class AWSIotTimeoutException extends Exception { + + /** The Constant serialVersionUID. */ + private static final long serialVersionUID = 1L; + + /** + * Instantiates a new exception object. + * + * @param message + * the error message + */ + public AWSIotTimeoutException(String message) { + super(message); + } + + /** + * Instantiates a new exception object. + * + * @param cause + * the cause. A null value is permitted, and indicates that the + * cause is nonexistent or unknown. + */ + public AWSIotTimeoutException(Throwable cause) { + super(cause); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotTopic.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotTopic.java new file mode 100644 index 00000000000..f8c4da15d74 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/AWSIotTopic.java @@ -0,0 +1,71 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client; + +import com.amazonaws.services.iot.client.core.AwsIotTopicCallback; + +/** + * This class is used for subscribing to a topic in the subscription APIs, such + * as {@link AWSIotMqttClient#subscribe(AWSIotTopic topic)}. + *

+ * In contains a callback function, {@link #onMessage}, that is invoked when a + * subscribed message has arrived. In most cases, applications are expected to + * override the default {@link #onMessage} method in order to access the message + * payload. + *

+ *

+ * This class extends {@link AWSIotMessage}, therefore callback functions in + * {@link AWSIotMessage} can also be overridden if the application wishes to be + * invoked for the outcomes of the subscription API. For more details, please + * refer to {@link AWSIotMessage}. + *

+ */ +public class AWSIotTopic extends AWSIotMessage implements AwsIotTopicCallback { + + /** + * Instantiates a new topic object. + * + * @param topic + * the topic to be subscribed to + */ + public AWSIotTopic(String topic) { + super(topic, AWSIotQos.QOS0); + } + + /** + * Instantiates a new topic object. + * + * @param topic + * the topic to be subscribed to + * @param qos + * the MQTT QoS level for the subscription + */ + public AWSIotTopic(String topic, AWSIotQos qos) { + super(topic, qos); + } + + /** + * Callback function to be invoked upon the arrival of a subscribed message. + * + * @param message + * the message received + */ + @Override + public void onMessage(AWSIotMessage message) { + // Default callback implementation is no-op + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AbstractAwsIotClient.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AbstractAwsIotClient.java new file mode 100644 index 00000000000..45ab35b4ae3 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AbstractAwsIotClient.java @@ -0,0 +1,398 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.core; + +import java.security.KeyStore; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import com.amazonaws.services.iot.client.AWSIotConfig; +import com.amazonaws.services.iot.client.AWSIotConnectionStatus; +import com.amazonaws.services.iot.client.AWSIotDevice; +import com.amazonaws.services.iot.client.AWSIotException; +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.AWSIotQos; +import com.amazonaws.services.iot.client.AWSIotTimeoutException; +import com.amazonaws.services.iot.client.AWSIotTopic; +import com.amazonaws.services.iot.client.shadow.AbstractAwsIotDevice; + +import lombok.Getter; +import lombok.Setter; + +/** + * The actual implementation of {@code AWSIotMqttClient}. + */ +@Getter +@Setter +public abstract class AbstractAwsIotClient implements AwsIotConnectionCallback { + + private static final Logger LOGGER = Logger.getLogger(AbstractAwsIotClient.class.getName()); + + protected final String clientId; + protected final String clientEndpoint; + protected final AwsIotConnectionType connectionType; + + protected int numOfClientThreads = AWSIotConfig.NUM_OF_CLIENT_THREADS; + protected int connectionTimeout = AWSIotConfig.CONNECTION_TIMEOUT; + protected int serverAckTimeout = AWSIotConfig.SERVER_ACK_TIMEOUT; + protected int keepAliveInterval = AWSIotConfig.KEEP_ALIVE_INTERVAL; + protected int maxConnectionRetries = AWSIotConfig.MAX_CONNECTION_RETRIES; + protected int baseRetryDelay = AWSIotConfig.CONNECTION_BASE_RETRY_DELAY; + protected int maxRetryDelay = AWSIotConfig.CONNECTION_MAX_RETRY_DELAY; + protected int maxOfflineQueueSize = AWSIotConfig.MAX_OFFLINE_QUEUE_SIZE; + protected AWSIotMessage willMessage; + + private final ConcurrentMap subscriptions = new ConcurrentHashMap<>(); + private final ConcurrentMap devices = new ConcurrentHashMap<>(); + private final AwsIotConnection connection; + + private ScheduledExecutorService executionService; + + protected AbstractAwsIotClient(String clientEndpoint, String clientId, KeyStore keyStore, String keyPassword) { + this.clientEndpoint = clientEndpoint; + this.clientId = clientId; + this.connectionType = AwsIotConnectionType.MQTT_OVER_TLS; + + try { + connection = new AwsIotTlsConnection(this, keyStore, keyPassword); + } catch (AWSIotException e) { + throw new AwsIotRuntimeException(e); + } + } + + protected AbstractAwsIotClient(String clientEndpoint, String clientId, String awsAccessKeyId, + String awsSecretAccessKey, String sessionToken) { + this.clientEndpoint = clientEndpoint; + this.clientId = clientId; + this.connectionType = AwsIotConnectionType.MQTT_OVER_WEBSOCKET; + + try { + connection = new AwsIotWebsocketConnection(this, awsAccessKeyId, awsSecretAccessKey, sessionToken); + } catch (AWSIotException e) { + throw new AwsIotRuntimeException(e); + } + } + + AbstractAwsIotClient(String clientEndpoint, String clientId, AwsIotConnection connection) { + this.clientEndpoint = clientEndpoint; + this.clientId = clientId; + this.connection = connection; + this.connectionType = null; + } + + public void connect() throws AWSIotException { + try { + connect(0, true); + } catch (AWSIotTimeoutException e) { + // We shouldn't get timeout exception because timeout is 0 + throw new AwsIotRuntimeException(e); + } + } + + public void connect(long timeout) throws AWSIotException, AWSIotTimeoutException { + connect(timeout, true); + } + + public void connect(long timeout, boolean blocking) throws AWSIotException, AWSIotTimeoutException { + synchronized (this) { + if (executionService == null) { + executionService = Executors.newScheduledThreadPool(numOfClientThreads); + } + } + + AwsIotCompletion completion = new AwsIotCompletion(timeout, !blocking); + connection.connect(completion); + completion.get(this); + } + + public void disconnect() throws AWSIotException { + try { + disconnect(0, true); + } catch (AWSIotTimeoutException e) { + // We shouldn't get timeout exception because timeout is 0 + throw new AwsIotRuntimeException(e); + } + } + + public void disconnect(long timeout) throws AWSIotException, AWSIotTimeoutException { + disconnect(timeout, true); + } + + public void disconnect(long timeout, boolean blocking) throws AWSIotException, AWSIotTimeoutException { + AwsIotCompletion completion = new AwsIotCompletion(timeout, !blocking); + connection.disconnect(completion); + completion.get(this); + } + + public void publish(String topic, String payload) throws AWSIotException { + publish(topic, AWSIotQos.QOS0, payload); + } + + public void publish(String topic, String payload, long timeout) throws AWSIotException, AWSIotTimeoutException { + publish(topic, AWSIotQos.QOS0, payload, timeout); + } + + public void publish(String topic, AWSIotQos qos, String payload) throws AWSIotException { + try { + publish(topic, qos, payload, 0); + } catch (AWSIotTimeoutException e) { + // We shouldn't get timeout exception because timeout is 0 + throw new AwsIotRuntimeException(e); + } + } + + public void publish(String topic, AWSIotQos qos, String payload, long timeout) + throws AWSIotException, AWSIotTimeoutException { + AwsIotCompletion completion = new AwsIotCompletion(topic, qos, payload, timeout); + connection.publish(completion); + completion.get(this); + } + + public void publish(String topic, byte[] payload) throws AWSIotException { + publish(topic, AWSIotQos.QOS0, payload); + } + + public void publish(String topic, byte[] payload, long timeout) throws AWSIotException, AWSIotTimeoutException { + publish(topic, AWSIotQos.QOS0, payload, timeout); + } + + public void publish(String topic, AWSIotQos qos, byte[] payload) throws AWSIotException { + try { + publish(topic, qos, payload, 0); + } catch (AWSIotTimeoutException e) { + // We shouldn't get timeout exception because timeout is 0 + throw new AwsIotRuntimeException(e); + } + } + + public void publish(String topic, AWSIotQos qos, byte[] payload, long timeout) + throws AWSIotException, AWSIotTimeoutException { + AwsIotCompletion completion = new AwsIotCompletion(topic, qos, payload, timeout); + connection.publish(completion); + completion.get(this); + } + + public void publish(AWSIotMessage message) throws AWSIotException { + publish(message, 0); + } + + public void publish(AWSIotMessage message, long timeout) throws AWSIotException { + AwsIotCompletion completion = new AwsIotCompletion(message, timeout, true); + connection.publish(completion); + try { + completion.get(this); + } catch (AWSIotTimeoutException e) { + // We shouldn't get timeout exception because it's asynchronous call + throw new AwsIotRuntimeException(e); + } + } + + public void subscribe(AWSIotTopic topic, boolean blocking) throws AWSIotException { + try { + _subscribe(topic, 0, !blocking); + } catch (AWSIotTimeoutException e) { + // We shouldn't get timeout exception because timeout is 0 + throw new AwsIotRuntimeException(e); + } + } + + public void subscribe(AWSIotTopic topic, long timeout, boolean blocking) + throws AWSIotException, AWSIotTimeoutException { + _subscribe(topic, timeout, !blocking); + } + + public void subscribe(AWSIotTopic topic) throws AWSIotException { + subscribe(topic, 0); + } + + public void subscribe(AWSIotTopic topic, long timeout) throws AWSIotException { + try { + _subscribe(topic, timeout, true); + } catch (AWSIotTimeoutException e) { + // We shouldn't get timeout exception because it's asynchronous call + throw new AwsIotRuntimeException(e); + } + } + + private void _subscribe(AWSIotTopic topic, long timeout, boolean async) + throws AWSIotException, AWSIotTimeoutException { + AwsIotCompletion completion = new AwsIotCompletion(topic, timeout, async); + connection.subscribe(completion); + completion.get(this); + + subscriptions.put(topic.getTopic(), topic); + } + + public void unsubscribe(String topic) throws AWSIotException { + try { + unsubscribe(topic, 0); + } catch (AWSIotTimeoutException e) { + // We shouldn't get timeout exception because timeout is 0 + throw new AwsIotRuntimeException(e); + } + } + + public void unsubscribe(String topic, long timeout) throws AWSIotException, AWSIotTimeoutException { + if (subscriptions.remove(topic) == null) { + return; + } + + AwsIotCompletion completion = new AwsIotCompletion(topic, AWSIotQos.QOS0, timeout); + connection.unsubscribe(completion); + completion.get(this); + } + + public void unsubscribe(AWSIotTopic topic) throws AWSIotException { + unsubscribe(topic, 0); + } + + public void unsubscribe(AWSIotTopic topic, long timeout) throws AWSIotException { + if (subscriptions.remove(topic.getTopic()) == null) { + return; + } + + AwsIotCompletion completion = new AwsIotCompletion(topic, timeout, true); + connection.unsubscribe(completion); + try { + completion.get(this); + } catch (AWSIotTimeoutException e) { + // We shouldn't get timeout exception because it's asynchronous call + throw new AwsIotRuntimeException(e); + } + } + + public void dispatch(AWSIotMessage message) { + AWSIotTopic topic = subscriptions.get(message.getTopic()); + if (topic != null) { + topic.onMessage(message); + } else { + LOGGER.warning("Unexpected message received from topic " + message.getTopic()); + } + } + + public void attach(AWSIotDevice device) throws AWSIotException { + if (devices.putIfAbsent(device.getThingName(), device) != null) { + return; + } + + device.setClient(this); + + // start the shadow sync task if the connection is already established + if (getConnectionStatus().equals(AWSIotConnectionStatus.CONNECTED)) { + device.activate(); + } + } + + public void detach(AWSIotDevice device) throws AWSIotException { + if (devices.remove(device.getThingName()) == null) { + return; + } + + device.deactivate(); + } + + public AWSIotConnectionStatus getConnectionStatus() { + if (connection != null) { + return connection.getConnectionStatus(); + } else { + return AWSIotConnectionStatus.DISCONNECTED; + } + } + + @Override + public void onConnectionSuccess() { + LOGGER.info("Client connection active: " + clientId); + + try { + // resubscribe all the subscriptions + for (AWSIotTopic topic : subscriptions.values()) { + subscribe(topic, serverAckTimeout); + } + + // start device sync + for (AbstractAwsIotDevice device : devices.values()) { + device.activate(); + } + } catch (AWSIotException e) { + // connection couldn't be fully recovered, disconnecting + LOGGER.warning("Failed to complete subscriptions while client is active, will disconnect"); + try { + connection.disconnect(null); + } catch (AWSIotException de) { + // ignore disconnect errors + } + } + } + + @Override + public void onConnectionFailure() { + LOGGER.info("Client connection lost: " + clientId); + + // stop device sync + for (AbstractAwsIotDevice device : devices.values()) { + try { + device.deactivate(); + } catch (AWSIotException e) { + // ignore errors from deactivate() as the connection is lost + LOGGER.warning("Failed to deactive all the devices, ignoring the error"); + } + } + } + + @Override + public void onConnectionClosed() { + LOGGER.info("Client connection closed: " + clientId); + + // stop device sync + for (AbstractAwsIotDevice device : devices.values()) { + try { + device.deactivate(); + } catch (AWSIotException e) { + // ignore errors from deactivate() as the connection is lost + LOGGER.warning("Failed to deactive all the devices, ignoring the error"); + } + } + + subscriptions.clear(); + devices.clear(); + + executionService.shutdown(); + } + + public Future scheduleTask(Runnable runnable) { + return scheduleTimeoutTask(runnable, 0); + } + + public Future scheduleTimeoutTask(Runnable runnable, long timeout) { + if (executionService == null) { + throw new AwsIotRuntimeException("Client is not connected"); + } + return executionService.schedule(runnable, timeout, TimeUnit.MILLISECONDS); + } + + public Future scheduleRoutineTask(Runnable runnable, long initialDelay, long period) { + if (executionService == null) { + throw new AwsIotRuntimeException("Client is not connected"); + } + return executionService.scheduleAtFixedRate(runnable, initialDelay, period, TimeUnit.MILLISECONDS); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotCompletion.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotCompletion.java new file mode 100644 index 00000000000..9bc6d402396 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotCompletion.java @@ -0,0 +1,310 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.core; + +import java.util.concurrent.Future; + +import com.amazonaws.services.iot.client.AWSIotException; +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.AWSIotQos; +import com.amazonaws.services.iot.client.AWSIotTimeoutException; + +/** + * This is a helper class that can be used to manage the request execution and + * return either synchronously or asynchronously the result, e.g. success, + * failure, or timeout. It's used by most of the APIs to implement blocking and + * non-blocking calls with timeout support. + */ +public class AwsIotCompletion extends AWSIotMessage { + + /** The request containing the callback functions. */ + protected final AWSIotMessage request; + + /** The timeout associated with the request. */ + protected final long timeout; + + /** whether the request is asynchronous or not. */ + protected final boolean isAsync; + + /** The future object of the timeout task. */ + protected Future timeoutTask; + + /** Indicates whether the request has completed successfully. */ + protected boolean hasSuccess; + + /** Indicates whether the request has completed with failure. */ + protected boolean hasFailure; + + /** Indicates whether the request has timed out. */ + protected boolean hasTimeout; + + /** + * Instantiates a new completion object with a synchronous request. + * + * @param topic + * the topic of the request + * @param qos + * the QoS of the request + * @param timeout + * the timeout in milliseconds for the request. If timeout is 0 + * or less, the request will never be timed out. + */ + public AwsIotCompletion(String topic, AWSIotQos qos, long timeout) { + super(topic, qos); + + this.timeout = timeout; + this.request = null; + this.isAsync = false; + } + + /** + * Instantiates a new completion object with a synchronous request. + * + * @param topic + * the topic of the request + * @param qos + * the QoS of the request + * @param payload + * the string payload of the request + * @param timeout + * the timeout in milliseconds for the request. If timeout is 0 + * or less, the request will never be timed out. + */ + public AwsIotCompletion(String topic, AWSIotQos qos, String payload, long timeout) { + super(topic, qos, payload); + + this.timeout = timeout; + this.request = null; + this.isAsync = false; + } + + /** + * Instantiates a new completion object with a synchronous request. + * + * @param topic + * the topic of the request + * @param qos + * the QoS of the request + * @param payload + * the byte array payload of the request + * @param timeout + * the timeout in milliseconds for the request. If timeout is 0 + * or less, the request will never be timed out. + */ + public AwsIotCompletion(String topic, AWSIotQos qos, byte[] payload, long timeout) { + super(topic, qos, payload); + + this.timeout = timeout; + this.request = null; + this.isAsync = false; + } + + /** + * Instantiates a new completion object either synchronous or asynchronous + * request based on the isAsync argument. + * + * @param timeout + * the timeout in milliseconds for the request. If timeout is 0 + * or less, the request will never be timed out. + * @param isAsync + * whether or not the request is asynchronous + */ + public AwsIotCompletion(long timeout, boolean isAsync) { + super(null, null); + + this.request = null; + this.timeout = timeout; + this.isAsync = isAsync; + } + + /** + * Instantiates a new completion object either synchronous or asynchronous + * request based on the isAsync argument. Callback functions + * are provided through the req argument. + * + * @param req + * the request containing request topic, QoS, payload, and + * callback functions for asynchronous requests. + * @param timeout + * the timeout in milliseconds for the request. If timeout is 0 + * or less, the request will never be timed out. + * @param isAsync + * whether or not the request is asynchronous + */ + public AwsIotCompletion(AWSIotMessage req, long timeout, boolean isAsync) { + super(req.getTopic(), req.getQos(), req.getPayload()); + + this.request = req; + this.timeout = timeout; + this.isAsync = isAsync; + } + + /** + * The user of the completion object is expected to call this function to + * either block until the request is completed or timed out in the case of + * synchronous calls, or to schedule a timeout handler in the case of + * asynchronous calls. + * + * @param client + * the client object that provides the execution thread pool for + * the timeout handler. + * @throws AWSIotException + * For synchronous calls, this exception may be thrown if the + * request has failed. + * @throws AWSIotTimeoutException + * For synchronous calls, this exception may be thrown if the + * request has timed out. + */ + public void get(AbstractAwsIotClient client) throws AWSIotException, AWSIotTimeoutException { + synchronized (this) { + if (hasSuccess || hasFailure || hasTimeout) { + // operation has completed before get() is called + if (!isAsync) { + if (hasFailure) { + throw new AWSIotException("Error happened when processing command " + topic); + } + if (hasTimeout) { + throw new AWSIotTimeoutException("Request timed out when processing command " + topic); + } + } + return; + } + + if (timeout > 0) { + timeoutTask = client.scheduleTimeoutTask(new Runnable() { + @Override + public void run() { + onTimeout(); + } + }, timeout); + } + + // if it's an asynchronous request, we don't block the calling + // thread + if (isAsync) { + return; + } + + while (!hasSuccess && !hasFailure && !hasTimeout) { + try { + wait(); + } catch (InterruptedException e) { + cancelTimeoutTask(); + throw new AWSIotException(e); + } + } + + cancelTimeoutTask(); + + if (hasFailure) { + throw new AWSIotException(errorCode, errorMessage); + } + if (hasTimeout) { + throw new AWSIotTimeoutException("Request timed out when processing request " + topic); + } + } + } + + /* + * (non-Javadoc) + * + * @see com.amazonaws.services.iot.client.AwsIotMessage#onSuccess() + */ + @Override + public void onSuccess() { + synchronized (this) { + if (hasSuccess || hasFailure || hasTimeout) { + return; + } + + hasSuccess = true; + cancelTimeoutTask(); + + if (!isAsync) { + notify(); + return; + } + } + + if (request != null) { + request.onSuccess(); + } + } + + /* + * (non-Javadoc) + * + * @see com.amazonaws.services.iot.client.AwsIotMessage#onFailure() + */ + @Override + public void onFailure() { + synchronized (this) { + if (hasSuccess || hasFailure || hasTimeout) { + return; + } + + hasFailure = true; + cancelTimeoutTask(); + + if (!isAsync) { + notify(); + return; + } + } + + if (request != null) { + request.setErrorCode(errorCode); + request.setErrorMessage(errorMessage); + request.onFailure(); + } + } + + /* + * (non-Javadoc) + * + * @see com.amazonaws.services.iot.client.AwsIotMessage#onTimeout() + */ + @Override + public void onTimeout() { + synchronized (this) { + if (hasSuccess || hasFailure || hasTimeout) { + return; + } + + hasTimeout = true; + cancelTimeoutTask(); + + if (!isAsync) { + notify(); + return; + } + } + + if (request != null) { + request.onTimeout(); + } + } + + /** + * Cancel timeout task. + */ + private void cancelTimeoutTask() { + if (timeoutTask != null && !timeoutTask.isCancelled()) { + timeoutTask.cancel(false); + } + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotConnection.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotConnection.java new file mode 100644 index 00000000000..db337e7356a --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotConnection.java @@ -0,0 +1,454 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.core; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Future; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.amazonaws.services.iot.client.AWSIotConnectionStatus; +import com.amazonaws.services.iot.client.AWSIotException; +import com.amazonaws.services.iot.client.AWSIotMessage; + +import lombok.Getter; +import lombok.Setter; + +/** + * This class provides an abstract layer for the library to communicate with the + * AWS IoT service without having to directly interact with the actual MQTT + * implementation. The abstraction layer also provides connection retry logic as + * well as offline message queuing. + */ +public abstract class AwsIotConnection implements AwsIotConnectionCallback { + + private static final Logger LOGGER = Logger.getLogger(AwsIotConnection.class.getName()); + + /** + * The client the connection is associated with. + * + * @return the current client + */ + @Getter + protected AbstractAwsIotClient client; + + /** + * The connection status. + * + * @param connectionStatus + * the new connection status + * @return the current connection status + */ + @Getter + @Setter + protected AWSIotConnectionStatus connectionStatus = AWSIotConnectionStatus.DISCONNECTED; + + /** + * The future object holding the retry task. + * + * @return the current retry task + */ + @Getter + private Future retryTask; + + /** + * The retry times. + * + * @return the current retry times + */ + @Getter + private int retryTimes; + + /** + * The callback functions for the connect request. + * + * @return the current connect callback + */ + @Getter + private AwsIotMessageCallback connectCallback; + + /** + * Flag to indicate user disconnect is in progress. + * + * @return the current user disconnect flag + */ + @Getter + private boolean userDisconnect; + + /** + * The offline publish queue holding messages while the connection is being + * established. + * + * @return the current offline publish queue + */ + @Getter + private ConcurrentLinkedQueue publishQueue = new ConcurrentLinkedQueue<>(); + + /** + * The offline subscribe request queue holding messages while the connection + * is being established. + * + * @return the current offline subscribe request queue + */ + @Getter + private ConcurrentLinkedQueue subscribeQueue = new ConcurrentLinkedQueue<>(); + + /** + * The offline unsubscribe request queue holding messages while the + * connection is being established. + * + * @return the current offline unsubscribe request queue + */ + @Getter + private ConcurrentLinkedQueue unsubscribeQueue = new ConcurrentLinkedQueue<>(); + + /** + * Instantiates a new connection object. + * + * @param client + * the client + */ + public AwsIotConnection(AbstractAwsIotClient client) { + this.client = client; + } + + /** + * Abstract method which is called to establish an underneath connection. + * + * @param callback + * connection callback functions + * @throws AWSIotException + * this exception is thrown when the request is failed to be + * sent + */ + protected abstract void openConnection(AwsIotMessageCallback callback) throws AWSIotException; + + /** + * Abstract method which is called to terminate an underneath connection. + * + * @param callback + * connection callback functions + * @throws AWSIotException + * this exception is thrown when the request is failed to be + * sent + */ + protected abstract void closeConnection(AwsIotMessageCallback callback) throws AWSIotException; + + /** + * Abstract method which is called to publish a message. + * + * @param message + * the message to be published + * @throws AWSIotException + * this exception is thrown when there's an unrecoverable error + * happened while processing the request + * @throws AwsIotRetryableException + * this exception is thrown when the request is failed to be + * sent, which will be queued and retried + */ + protected abstract void publishMessage(AWSIotMessage message) throws AWSIotException, AwsIotRetryableException; + + /** + * Abstract method which is called to subscribe to a topic. + * + * @param message + * the topic to be subscribed to + * @throws AWSIotException + * this exception is thrown when there's an unrecoverable error + * happened while processing the request + * @throws AwsIotRetryableException + * this exception is thrown when the request is failed to be + * sent, which will be queued and retried + */ + protected abstract void subscribeTopic(AWSIotMessage message) throws AWSIotException, AwsIotRetryableException; + + /** + * Abstract method which is called to unsubscribe to a topic. + * + * @param message + * the topic to be unsubscribed to + * @throws AWSIotException + * this exception is thrown when there's an unrecoverable error + * happened while processing the request + * @throws AwsIotRetryableException + * this exception is thrown when the request is failed to be + * sent, which will be queued and retried + */ + protected abstract void unsubscribeTopic(AWSIotMessage message) throws AWSIotException, AwsIotRetryableException; + + /** + * The actual publish method exposed by this class. + * + * @param message + * the message to be published + * @throws AWSIotException + * this exception is thrown when the underneath failed to + * process the request + */ + public void publish(AWSIotMessage message) throws AWSIotException { + try { + publishMessage(message); + } catch (AwsIotRetryableException e) { + if (client.getMaxOfflineQueueSize() > 0 && publishQueue.size() < client.getMaxOfflineQueueSize()) { + publishQueue.add(message); + } else { + LOGGER.info("Failed to publish message to " + message.getTopic()); + throw new AWSIotException(e); + } + } + } + + /** + * The actual subscribe method exposed by this class. + * + * @param message + * the topic to be subscribed to + * @throws AWSIotException + * this exception is thrown when the underneath failed to + * process the request + */ + public void subscribe(AWSIotMessage message) throws AWSIotException { + try { + subscribeTopic(message); + } catch (AwsIotRetryableException e) { + if (client.getMaxOfflineQueueSize() > 0 && subscribeQueue.size() < client.getMaxOfflineQueueSize()) { + subscribeQueue.add(message); + } else { + LOGGER.info("Failed to subscribe to " + message.getTopic()); + throw new AWSIotException(e); + } + } + + } + + /** + * The actual unsubscribe method exposed by this class. + * + * @param message + * the topic to be unsubscribed to + * @throws AWSIotException + * this exception is thrown when the underneath failed to + * process the request + */ + public void unsubscribe(AWSIotMessage message) throws AWSIotException { + try { + unsubscribeTopic(message); + } catch (AwsIotRetryableException e) { + if (client.getMaxOfflineQueueSize() > 0 && unsubscribeQueue.size() < client.getMaxOfflineQueueSize()) { + unsubscribeQueue.add(message); + } else { + LOGGER.info("Failed to unsubscribe to " + message.getTopic()); + throw new AWSIotException(e); + } + } + + } + + /** + * The actual connect method exposed by this class. + * + * @param callback + * user callback functions + * @throws AWSIotException + * this exception is thrown when the underneath layer failed to + * process the request + */ + public void connect(AwsIotMessageCallback callback) throws AWSIotException { + cancelRetry(); + + retryTimes = 0; + userDisconnect = false; + connectCallback = callback; + + openConnection(null); + } + + /** + * The actual disconnect method exposed by this class. + * + * @param callback + * user callback functions + * @throws AWSIotException + * this exception is thrown when the underneath layer failed to + * process the request + */ + public void disconnect(AwsIotMessageCallback callback) throws AWSIotException { + cancelRetry(); + + retryTimes = 0; + userDisconnect = true; + connectCallback = null; + + closeConnection(callback); + } + + /* + * (non-Javadoc) + * + * @see com.amazonaws.services.iot.client.core.AwsIotConnectionCallback# + * onConnectionSuccess() + */ + @Override + public void onConnectionSuccess() { + LOGGER.info("Connection successfully established"); + + connectionStatus = AWSIotConnectionStatus.CONNECTED; + retryTimes = 0; + + cancelRetry(); + + // process offline messages + try { + while (subscribeQueue.size() > 0) { + AWSIotMessage message = subscribeQueue.poll(); + subscribeTopic(message); + } + while (unsubscribeQueue.size() > 0) { + AWSIotMessage message = unsubscribeQueue.poll(); + unsubscribeTopic(message); + } + while (publishQueue.size() > 0) { + AWSIotMessage message = publishQueue.poll(); + publishMessage(message); + } + } catch (AWSIotException | AwsIotRetryableException e) { + // should close the connection if we can't send message when + // connection is good + LOGGER.log(Level.WARNING, "Failed to send queued messages, will disconnect", e); + try { + closeConnection(null); + } catch (AWSIotException ie) { + LOGGER.log(Level.WARNING, "Failed to disconnect", ie); + } + } + + client.onConnectionSuccess(); + + if (connectCallback != null) { + connectCallback.onSuccess(); + connectCallback = null; + } + } + + /* + * (non-Javadoc) + * + * @see com.amazonaws.services.iot.client.core.AwsIotConnectionCallback# + * onConnectionFailure() + */ + @Override + public void onConnectionFailure() { + LOGGER.info("Connection temporarily lost"); + + connectionStatus = AWSIotConnectionStatus.DISCONNECTED; + + cancelRetry(); + + if (shouldRetry()) { + retryConnection(); + client.onConnectionFailure(); + } else { + // permanent failure, notify the client and no more retries + LOGGER.info("Connection retry cancelled or exceeded maximum retries"); + if (connectCallback != null) { + connectCallback.onFailure(); + connectCallback = null; + } + + client.onConnectionClosed(); + } + } + + /* + * (non-Javadoc) + * + * @see com.amazonaws.services.iot.client.core.AwsIotConnectionCallback# + * onConnectionClosed() + */ + @Override + public void onConnectionClosed() { + LOGGER.info("Connection permanently closed"); + + connectionStatus = AWSIotConnectionStatus.DISCONNECTED; + + cancelRetry(); + + if (connectCallback != null) { + connectCallback.onFailure(); + connectCallback = null; + } + + client.onConnectionClosed(); + } + + /** + * Whether or not to reestablish the connection. + * + * @return true, if successful + */ + private boolean shouldRetry() { + return (!userDisconnect && (client.getMaxConnectionRetries() > 0 && retryTimes < client + .getMaxConnectionRetries())); + } + + /** + * Cancel any pending retry request. + */ + private void cancelRetry() { + if (retryTask != null) { + retryTask.cancel(false); + retryTask = null; + } + } + + /** + * Gets the exponentially back-off retry delay based on the number of times + * the connection has been retried. + * + * @return the retry delay + */ + private long getRetryDelay() { + return Math.min(client.getBaseRetryDelay() * (long) Math.pow(2, retryTimes), client.getMaxRetryDelay()); + } + + /** + * Schedule retry task so the connection can be retried after the timeout + */ + private void retryConnection() { + if (retryTask != null) { + LOGGER.warning("Connection retry already in progress"); + // retry task already scheduled, do nothing + return; + } + + retryTask = client.scheduleTimeoutTask(new Runnable() { + @Override + public void run() { + LOGGER.info("Connection is being retried"); + + connectionStatus = AWSIotConnectionStatus.RECONNECTING; + retryTimes++; + try { + openConnection(null); + } catch (AWSIotException e) { + // permanent failure, notify the client and no more retries + client.onConnectionClosed(); + } + } + }, getRetryDelay()); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotConnectionCallback.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotConnectionCallback.java new file mode 100644 index 00000000000..766cdddd622 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotConnectionCallback.java @@ -0,0 +1,39 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.core; + +/** + * This interface class defines functions called under different connection + * events. + */ +public interface AwsIotConnectionCallback { + + /** + * On connection success. + */ + void onConnectionSuccess(); + + /** + * On connection failure. + */ + void onConnectionFailure(); + + /** + * On connection closed. + */ + void onConnectionClosed(); + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotConnectionType.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotConnectionType.java new file mode 100644 index 00000000000..1377fa3f368 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotConnectionType.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.core; + +/** + * Connection types supported by this library. + */ +public enum AwsIotConnectionType { + + /** The mqtt over tls. */ + MQTT_OVER_TLS, + + /** The mqtt over websocket. */ + MQTT_OVER_WEBSOCKET + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotMessageCallback.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotMessageCallback.java new file mode 100644 index 00000000000..0c7a084b507 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotMessageCallback.java @@ -0,0 +1,39 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.core; + +/** + * This interface class defines functions called under different message related + * events. + */ +public interface AwsIotMessageCallback { + + /** + * On success. + */ + void onSuccess(); + + /** + * On failure. + */ + void onFailure(); + + /** + * On timeout. + */ + void onTimeout(); + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotRetryableException.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotRetryableException.java new file mode 100644 index 00000000000..193889736ba --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotRetryableException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.core; + +/** + * This exception class is used internally in the library to track retryable + * events. + */ +public class AwsIotRetryableException extends Exception { + + private static final long serialVersionUID = 1L; + + public AwsIotRetryableException(String message) { + super(message); + } + + public AwsIotRetryableException(Throwable e) { + super(e); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotRuntimeException.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotRuntimeException.java new file mode 100644 index 00000000000..6dd1b6afaa7 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotRuntimeException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.core; + +/** + * This exception class is used internally in the library for runtime errors. + */ +public class AwsIotRuntimeException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public AwsIotRuntimeException(Throwable e) { + super(e); + } + + public AwsIotRuntimeException(String message) { + super(message); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotTlsConnection.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotTlsConnection.java new file mode 100644 index 00000000000..2b0e9dd08f0 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotTlsConnection.java @@ -0,0 +1,35 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.core; + +import java.security.KeyStore; + +import com.amazonaws.services.iot.client.AWSIotException; +import com.amazonaws.services.iot.client.mqtt.AwsIotMqttConnection; +import com.amazonaws.services.iot.client.util.AwsIotTlsSocketFactory; + +/** + * This is a thin layer on top of {@link AwsIotMqttConnection} that provides a + * TLS v1.2 based communication channel to the MQTT implementation. + */ +public class AwsIotTlsConnection extends AwsIotMqttConnection { + + public AwsIotTlsConnection(AbstractAwsIotClient client, KeyStore keyStore, String keyPassword) + throws AWSIotException { + super(client, new AwsIotTlsSocketFactory(keyStore, keyPassword), "ssl://" + client.getClientEndpoint() + ":8883"); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotTopicCallback.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotTopicCallback.java new file mode 100644 index 00000000000..0046afaca5c --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotTopicCallback.java @@ -0,0 +1,28 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.core; + +import com.amazonaws.services.iot.client.AWSIotMessage; + +/** + * This interface class defines the function called when subscribed message has + * arrived. + */ +public interface AwsIotTopicCallback { + + void onMessage(AWSIotMessage message); + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotWebsocketConnection.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotWebsocketConnection.java new file mode 100644 index 00000000000..59922658031 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/core/AwsIotWebsocketConnection.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.core; + +import java.util.HashSet; +import java.util.Set; + +import com.amazonaws.services.iot.client.AWSIotException; +import com.amazonaws.services.iot.client.mqtt.AwsIotMqttConnection; +import com.amazonaws.services.iot.client.util.AwsIotWebSocketUrlSigner; + +/** + * This is a thin layer on top of {@link AwsIotMqttConnection} that provides a + * WebSocket based communication channel to the MQTT implementation. + */ +public class AwsIotWebsocketConnection extends AwsIotMqttConnection { + + public AwsIotWebsocketConnection(AbstractAwsIotClient client, String awsAccessKeyId, String awsSecretAccessKey) + throws AWSIotException { + this(client, awsAccessKeyId, awsSecretAccessKey, null); + } + + public AwsIotWebsocketConnection(AbstractAwsIotClient client, String awsAccessKeyId, String awsSecretAccessKey, + String sessionToken) throws AWSIotException { + super(client, null, "wss://" + client.getClientEndpoint() + ":443"); + + // Port number must be included in the endpoint for signing otherwise + // the signature verification will fail. This is because the Paho client + // library (1.0.3) always includes port number in the host line of the HTTP + // request header, e.g "Host: data.iot.us-east-1.amazonaws.com:443". + String signedUrl = AwsIotWebSocketUrlSigner.getSignedUrl(client.getClientEndpoint() + ":443", awsAccessKeyId, + awsSecretAccessKey, sessionToken, null); + Set uris = new HashSet<>(); + uris.add(signedUrl); + this.setServerUris(uris); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/mqtt/AwsIotMqttClientListener.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/mqtt/AwsIotMqttClientListener.java new file mode 100644 index 00000000000..adf18653012 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/mqtt/AwsIotMqttClientListener.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.mqtt; + +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.AWSIotQos; +import com.amazonaws.services.iot.client.core.AbstractAwsIotClient; + +/** + * This class implements listener functions for client related events from the + * Paho MQTT library. + */ +public class AwsIotMqttClientListener implements MqttCallback { + + private AbstractAwsIotClient client; + + public AwsIotMqttClientListener(AbstractAwsIotClient client) { + this.client = client; + } + + @Override + public void connectionLost(Throwable arg0) { + client.scheduleTask(new Runnable() { + @Override + public void run() { + client.getConnection().onConnectionFailure(); + } + }); + } + + @Override + public void deliveryComplete(IMqttDeliveryToken arg0) { + // Callback is not used + } + + @Override + public void messageArrived(String topic, MqttMessage arg1) throws Exception { + final AWSIotMessage message = new AWSIotMessage(topic, AWSIotQos.valueOf(arg1.getQos()), arg1.getPayload()); + + client.scheduleTask(new Runnable() { + @Override + public void run() { + client.dispatch(message); + } + }); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/mqtt/AwsIotMqttConnection.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/mqtt/AwsIotMqttConnection.java new file mode 100644 index 00000000000..9422dad4b79 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/mqtt/AwsIotMqttConnection.java @@ -0,0 +1,170 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.mqtt; + +import java.util.HashSet; +import java.util.Set; + +import javax.net.SocketFactory; + +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; + +import com.amazonaws.services.iot.client.AWSIotException; +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.core.AbstractAwsIotClient; +import com.amazonaws.services.iot.client.core.AwsIotConnection; +import com.amazonaws.services.iot.client.core.AwsIotMessageCallback; +import com.amazonaws.services.iot.client.core.AwsIotRetryableException; + +import lombok.Getter; +import lombok.Setter; + +/** + * This class extends {@link AwsIotConnection} to provide the basic MQTT pub/sub + * functionalities using the Paho MQTT library. + */ +@Getter +@Setter +public class AwsIotMqttConnection extends AwsIotConnection { + + private final SocketFactory socketFactory; + + private MqttAsyncClient mqttClient; + private AwsIotMqttMessageListener messageListener; + private AwsIotMqttClientListener clientListener; + private Set serverUris; + + public AwsIotMqttConnection(AbstractAwsIotClient client, SocketFactory socketFactory, String serverUri) + throws AWSIotException { + super(client); + + this.socketFactory = socketFactory; + + messageListener = new AwsIotMqttMessageListener(client); + clientListener = new AwsIotMqttClientListener(client); + + try { + mqttClient = new MqttAsyncClient(serverUri, client.getClientId(), new MemoryPersistence()); + mqttClient.setCallback(clientListener); + } catch (MqttException e) { + throw new AWSIotException(e); + } + } + + AwsIotMqttConnection(AbstractAwsIotClient client, MqttAsyncClient mqttClient) throws AWSIotException { + super(client); + this.mqttClient = mqttClient; + this.socketFactory = null; + } + + public void openConnection(AwsIotMessageCallback callback) throws AWSIotException { + try { + AwsIotMqttConnectionListener connectionListener = new AwsIotMqttConnectionListener(client, true, callback); + MqttConnectOptions options = buildMqttConnectOptions(client, socketFactory); + mqttClient.connect(options, null, connectionListener); + } catch (MqttException e) { + throw new AWSIotException(e); + } + } + + public void closeConnection(AwsIotMessageCallback callback) throws AWSIotException { + try { + AwsIotMqttConnectionListener connectionListener = new AwsIotMqttConnectionListener(client, false, callback); + mqttClient.disconnect(0, null, connectionListener); + } catch (MqttException e) { + throw new AWSIotException(e); + } + } + + @Override + public void publishMessage(AWSIotMessage message) throws AWSIotException, AwsIotRetryableException { + String topic = message.getTopic(); + MqttMessage mqttMessage = new MqttMessage(message.getPayload()); + mqttMessage.setQos(message.getQos().getValue()); + + try { + mqttClient.publish(topic, mqttMessage, message, messageListener); + } catch (MqttException e) { + if (e.getReasonCode() == MqttException.REASON_CODE_CLIENT_NOT_CONNECTED) { + throw new AwsIotRetryableException(e); + } else { + throw new AWSIotException(e); + } + } + } + + @Override + public void subscribeTopic(AWSIotMessage message) throws AWSIotException, AwsIotRetryableException { + try { + mqttClient.subscribe(message.getTopic(), message.getQos().getValue(), message, messageListener); + } catch (MqttException e) { + if (e.getReasonCode() == MqttException.REASON_CODE_CLIENT_NOT_CONNECTED) { + throw new AwsIotRetryableException(e); + } else { + throw new AWSIotException(e); + } + } + } + + @Override + public void unsubscribeTopic(AWSIotMessage message) throws AWSIotException, AwsIotRetryableException { + try { + mqttClient.unsubscribe(message.getTopic(), message, messageListener); + } catch (MqttException e) { + if (e.getReasonCode() == MqttException.REASON_CODE_CLIENT_NOT_CONNECTED) { + throw new AwsIotRetryableException(e); + } else { + throw new AWSIotException(e); + } + } + } + + public Set getServerUris() { + return new HashSet<>(serverUris); + } + + public void setServerUris(Set serverUris) { + this.serverUris = new HashSet<>(serverUris); + } + + private MqttConnectOptions buildMqttConnectOptions(AbstractAwsIotClient client, SocketFactory socketFactory) { + MqttConnectOptions options = new MqttConnectOptions(); + + options.setSocketFactory(socketFactory); + options.setCleanSession(true); + options.setConnectionTimeout(client.getConnectionTimeout() / 1000); + options.setKeepAliveInterval(client.getKeepAliveInterval() / 1000); + + if (serverUris != null && !serverUris.isEmpty()) { + String[] uriArray = new String[serverUris.size()]; + serverUris.toArray(uriArray); + options.setServerURIs(uriArray); + } + + if (client.getWillMessage() != null) { + AWSIotMessage message = client.getWillMessage(); + + options.setWill(message.getTopic(), message.getPayload(), message.getQos().getValue(), false); + } + + return options; + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/mqtt/AwsIotMqttConnectionListener.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/mqtt/AwsIotMqttConnectionListener.java new file mode 100644 index 00000000000..e154918ee1c --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/mqtt/AwsIotMqttConnectionListener.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.mqtt; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.eclipse.paho.client.mqttv3.IMqttActionListener; +import org.eclipse.paho.client.mqttv3.IMqttToken; + +import com.amazonaws.services.iot.client.core.AbstractAwsIotClient; +import com.amazonaws.services.iot.client.core.AwsIotMessageCallback; + +/** + * This class implements listener functions for the connection events from the + * Paho MQTT library. + */ +public class AwsIotMqttConnectionListener implements IMqttActionListener { + + private static final Logger LOGGER = Logger.getLogger(AwsIotMqttConnectionListener.class.getName()); + + private final AbstractAwsIotClient client; + private final boolean isConnect; + private final AwsIotMessageCallback userCallback; + + public AwsIotMqttConnectionListener(AbstractAwsIotClient client, boolean isConnect, + AwsIotMessageCallback userCallback) { + this.client = client; + this.isConnect = isConnect; + this.userCallback = userCallback; + } + + @Override + public void onSuccess(IMqttToken arg0) { + client.scheduleTask(new Runnable() { + @Override + public void run() { + if (isConnect) { + client.getConnection().onConnectionSuccess(); + } else { + client.getConnection().onConnectionClosed(); + } + if (userCallback != null) { + userCallback.onSuccess(); + } + } + }); + } + + @Override + public void onFailure(IMqttToken arg0, Throwable arg1) { + LOGGER.log(Level.WARNING, (isConnect ? "Connect" : "Disconnect") + " request failure", arg1); + + client.scheduleTask(new Runnable() { + @Override + public void run() { + if (isConnect) { + client.getConnection().onConnectionFailure(); + } else { + client.getConnection().onConnectionClosed(); + } + if (userCallback != null) { + userCallback.onFailure(); + } + } + }); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/mqtt/AwsIotMqttMessageListener.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/mqtt/AwsIotMqttMessageListener.java new file mode 100644 index 00000000000..619e00573c6 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/mqtt/AwsIotMqttMessageListener.java @@ -0,0 +1,93 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.mqtt; + +import java.util.logging.Logger; + +import org.eclipse.paho.client.mqttv3.IMqttActionListener; +import org.eclipse.paho.client.mqttv3.IMqttToken; +import org.eclipse.paho.client.mqttv3.internal.wire.MqttSuback; + +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.core.AbstractAwsIotClient; + +/** + * This class implements listener functions for the message events from the Paho + * MQTT library. + */ +public class AwsIotMqttMessageListener implements IMqttActionListener { + + private static final Logger LOGGER = Logger.getLogger(AwsIotMqttMessageListener.class.getName()); + + private static final int SUB_ACK_RETURN_CODE_FAILURE = 0x80; + + private AbstractAwsIotClient client; + + public AwsIotMqttMessageListener(AbstractAwsIotClient client) { + this.client = client; + } + + @Override + public void onSuccess(IMqttToken token) { + final AWSIotMessage message = (AWSIotMessage) token.getUserContext(); + if (message == null) { + return; + } + + boolean forceFailure = false; + if (token.getResponse() instanceof MqttSuback) { + MqttSuback subAck = (MqttSuback) token.getResponse(); + int qos[] = subAck.getGrantedQos(); + for (int i = 0; i < qos.length; i++) { + if (qos[i] == SUB_ACK_RETURN_CODE_FAILURE) { + LOGGER.warning("Request failed: likely due to too many subscriptions or policy violations"); + forceFailure = true; + break; + } + } + } + + final boolean isSuccess = !forceFailure; + client.scheduleTask(new Runnable() { + @Override + public void run() { + if (isSuccess) { + message.onSuccess(); + } else { + message.onFailure(); + } + } + }); + } + + @Override + public void onFailure(IMqttToken token, Throwable cause) { + final AWSIotMessage message = (AWSIotMessage) token.getUserContext(); + if (message == null) { + LOGGER.warning("Request failed: " + token.getException()); + return; + } + + LOGGER.warning("Request failed for topic " + message.getTopic() + ": " + token.getException()); + client.scheduleTask(new Runnable() { + @Override + public void run() { + message.onFailure(); + } + }); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AbstractAwsIotDevice.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AbstractAwsIotDevice.java new file mode 100644 index 00000000000..8be76477286 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AbstractAwsIotDevice.java @@ -0,0 +1,329 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.shadow; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.amazonaws.services.iot.client.AWSIotConfig; +import com.amazonaws.services.iot.client.AWSIotDevice; +import com.amazonaws.services.iot.client.AWSIotDeviceProperty; +import com.amazonaws.services.iot.client.AWSIotException; +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.AWSIotQos; +import com.amazonaws.services.iot.client.AWSIotTimeoutException; +import com.amazonaws.services.iot.client.AWSIotTopic; +import com.amazonaws.services.iot.client.core.AbstractAwsIotClient; +import com.amazonaws.services.iot.client.shadow.AwsIotDeviceCommandManager.Command; +import com.amazonaws.services.iot.client.shadow.AwsIotDeviceCommandManager.CommandAck; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import lombok.Getter; +import lombok.Setter; + +/** + * The actual implementation of {@link AWSIotDevice}. + */ +@Getter +@Setter +public abstract class AbstractAwsIotDevice { + + private static final Logger LOGGER = Logger.getLogger(AbstractAwsIotDevice.class.getName()); + + protected final String thingName; + + protected long reportInterval = AWSIotConfig.DEVICE_REPORT_INTERVAL; + protected boolean enableVersioning = AWSIotConfig.DEVICE_ENABLE_VERSIONING; + protected AWSIotQos deviceReportQos = AWSIotQos.valueOf(AWSIotConfig.DEVICE_REPORT_QOS); + protected AWSIotQos shadowUpdateQos = AWSIotQos.valueOf(AWSIotConfig.DEVICE_SHADOW_UPDATE_QOS); + protected AWSIotQos methodQos = AWSIotQos.valueOf(AWSIotConfig.DEVICE_METHOD_QOS); + protected AWSIotQos methodAckQos = AWSIotQos.valueOf(AWSIotConfig.DEVICE_METHOD_ACK_QOS); + + private final Map reportedProperties; + private final Map updatableProperties; + private final AwsIotDeviceCommandManager commandManager; + private final ConcurrentMap deviceSubscriptions; + private final ObjectMapper jsonObjectMapper; + + private AbstractAwsIotClient client; + private Future syncTask; + private AtomicLong localVersion; + + protected AbstractAwsIotDevice(String thingName) { + this.thingName = thingName; + + reportedProperties = getDeviceProperties(true, false); + updatableProperties = getDeviceProperties(false, true); + commandManager = new AwsIotDeviceCommandManager(this); + + deviceSubscriptions = new ConcurrentHashMap<>(); + for (String topic : getDeviceTopics()) { + deviceSubscriptions.put(topic, false); + } + + jsonObjectMapper = new ObjectMapper(); + SimpleModule module = new SimpleModule(); + module.addSerializer(AbstractAwsIotDevice.class, new AwsIotJsonSerializer()); + jsonObjectMapper.registerModule(module); + + localVersion = new AtomicLong(-1); + } + + protected AbstractAwsIotDevice getDevice() { + return this; + } + + protected String get() throws AWSIotException { + AWSIotMessage message = new AWSIotMessage(null, methodQos); + return commandManager.runCommandSync(Command.GET, message); + } + + protected String get(long timeout) throws AWSIotException, AWSIotTimeoutException { + AWSIotMessage message = new AWSIotMessage(null, methodQos); + return commandManager.runCommandSync(Command.GET, message, timeout); + } + + protected void get(AWSIotMessage message, long timeout) throws AWSIotException { + commandManager.runCommand(Command.GET, message, timeout); + } + + protected void update(String jsonState) throws AWSIotException { + AWSIotMessage message = new AWSIotMessage(null, methodQos, jsonState); + commandManager.runCommandSync(Command.UPDATE, message); + } + + protected void update(String jsonState, long timeout) throws AWSIotException, AWSIotTimeoutException { + AWSIotMessage message = new AWSIotMessage(null, methodQos, jsonState); + commandManager.runCommandSync(Command.UPDATE, message, timeout); + } + + protected void update(AWSIotMessage message, long timeout) throws AWSIotException { + commandManager.runCommand(Command.UPDATE, message, timeout); + } + + protected void delete() throws AWSIotException { + AWSIotMessage message = new AWSIotMessage(null, methodQos); + commandManager.runCommandSync(Command.DELETE, message); + } + + protected void delete(long timeout) throws AWSIotException, AWSIotTimeoutException { + AWSIotMessage message = new AWSIotMessage(null, methodQos); + commandManager.runCommandSync(Command.DELETE, message, timeout); + } + + protected void delete(AWSIotMessage message, long timeout) throws AWSIotException { + commandManager.runCommand(Command.DELETE, message, timeout); + } + + protected void onShadowUpdate(String jsonState) { + // synchronized block to serialize device accesses + synchronized (this) { + try { + AwsIotJsonDeserializer.deserialize(this, jsonState); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to update device", e); + } + } + } + + protected String onDeviceReport() { + // synchronized block to serialize device accesses + synchronized (this) { + try { + return jsonObjectMapper.writeValueAsString(this); + } catch (JsonProcessingException e) { + LOGGER.log(Level.WARNING, "Failed to generate device report", e); + return null; + } + } + } + + public void activate() throws AWSIotException { + stopSync(); + + for (String topic : getDeviceTopics()) { + AWSIotTopic awsIotTopic; + + if (commandManager.isDeltaTopic(topic)) { + awsIotTopic = new AwsIotDeviceDeltaListener(topic, shadowUpdateQos, this); + } else { + awsIotTopic = new AwsIotDeviceCommandAckListener(topic, methodAckQos, this); + } + + client.subscribe(awsIotTopic, client.getServerAckTimeout()); + } + + startSync(); + } + + public void deactivate() throws AWSIotException { + stopSync(); + + commandManager.onDeactivate(); + + for (String topic : getDeviceTopics()) { + deviceSubscriptions.put(topic, false); + + AWSIotTopic awsIotTopic = new AWSIotTopic(topic); + client.unsubscribe(awsIotTopic, client.getServerAckTimeout()); + } + } + + public boolean isTopicReady(String topic) { + Boolean status = deviceSubscriptions.get(topic); + + return Boolean.TRUE.equals(status); + } + + public boolean isCommandReady(Command command) { + Boolean accepted = deviceSubscriptions.get(commandManager.getTopic(command, CommandAck.ACCEPTED)); + Boolean rejected = deviceSubscriptions.get(commandManager.getTopic(command, CommandAck.REJECTED)); + + return (Boolean.TRUE.equals(accepted) && Boolean.TRUE.equals(rejected)); + } + + public void onSubscriptionAck(String topic, boolean success) { + deviceSubscriptions.put(topic, success); + commandManager.onSubscriptionAck(topic, success); + } + + public void onCommandAck(AWSIotMessage message) { + commandManager.onCommandAck(message); + } + + protected void startSync() { + // don't start the publish task if no properties are to be published + if (reportedProperties.isEmpty() || reportInterval <= 0) { + return; + } + + syncTask = client.scheduleRoutineTask(new Runnable() { + @Override + public void run() { + if (!isCommandReady(Command.UPDATE)) { + LOGGER.fine("Device not ready for reporting"); + return; + } + + long reportVersion = localVersion.get(); + if (enableVersioning && reportVersion < 0) { + // if versioning is enabled, synchronize the version first + LOGGER.fine("Starting version sync"); + startVersionSync(); + return; + } + + String jsonState = onDeviceReport(); + if (jsonState != null) { + LOGGER.fine("Sending device report"); + sendDeviceReport(reportVersion, jsonState); + } + } + }, 0l, reportInterval); + } + + protected void stopSync() { + if (syncTask != null) { + syncTask.cancel(false); + syncTask = null; + } + + localVersion.set(-1); + } + + protected void startVersionSync() { + localVersion.set(-1); + + AwsIotDeviceSyncMessage message = new AwsIotDeviceSyncMessage(null, shadowUpdateQos, this); + try { + commandManager.runCommand(Command.GET, message, client.getServerAckTimeout(), true); + } catch (AWSIotTimeoutException e) { + // async command, shouldn't receive timeout exception + } catch (AWSIotException e) { + LOGGER.log(Level.WARNING, "Failed to publish version update message", e); + } + } + + private void sendDeviceReport(long reportVersion, String jsonState) { + StringBuilder payload = new StringBuilder("{"); + + if (enableVersioning) { + payload.append("\"version\":").append(reportVersion).append(","); + } + payload.append("\"state\":{\"reported\":").append(jsonState).append("}}"); + + AwsIotDeviceReportMessage message = new AwsIotDeviceReportMessage(null, shadowUpdateQos, reportVersion, + payload.toString(), this); + if (enableVersioning && reportVersion != localVersion.get()) { + LOGGER.warning("Local version number has changed, skip reporting for this round"); + return; + } + + try { + commandManager.runCommand(Command.UPDATE, message, client.getServerAckTimeout(), true); + } catch (AWSIotTimeoutException e) { + // async command, shouldn't receive timeout exception + } catch (AWSIotException e) { + LOGGER.log(Level.WARNING, "Failed to publish device report message", e); + } + } + + private Map getDeviceProperties(boolean enableReport, boolean allowUpdate) { + Map properties = new HashMap<>(); + + for (Field field : this.getClass().getDeclaredFields()) { + AWSIotDeviceProperty annotation = field.getAnnotation(AWSIotDeviceProperty.class); + if (annotation == null) { + continue; + } + + String propertyName = annotation.name().length() > 0 ? annotation.name() : field.getName(); + if ((enableReport && annotation.enableReport()) || (allowUpdate && annotation.allowUpdate())) { + properties.put(propertyName, field); + } + } + + return properties; + } + + private List getDeviceTopics() { + List topics = new ArrayList<>(); + + topics.add(commandManager.getTopic(Command.DELTA, null)); + + topics.add(commandManager.getTopic(Command.GET, CommandAck.ACCEPTED)); + topics.add(commandManager.getTopic(Command.GET, CommandAck.REJECTED)); + topics.add(commandManager.getTopic(Command.UPDATE, CommandAck.ACCEPTED)); + topics.add(commandManager.getTopic(Command.UPDATE, CommandAck.REJECTED)); + topics.add(commandManager.getTopic(Command.DELETE, CommandAck.ACCEPTED)); + topics.add(commandManager.getTopic(Command.DELETE, CommandAck.REJECTED)); + + return topics; + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceCommand.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceCommand.java new file mode 100644 index 00000000000..953eeea8a7a --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceCommand.java @@ -0,0 +1,121 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.shadow; + +import java.util.logging.Logger; + +import com.amazonaws.services.iot.client.AWSIotException; +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.AWSIotTimeoutException; +import com.amazonaws.services.iot.client.core.AwsIotCompletion; +import com.amazonaws.services.iot.client.shadow.AwsIotDeviceCommandManager.Command; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +/** + * This is a helper class that can be used to manage the execution result of a + * shadow command, i.e. get, update, and delete. It makes sure that the command + * is not published until the subscription requests for the acknowledgment + * topics, namely accepted and rejected, have completed successfully. + * + * @see com.amazonaws.services.iot.client.core.AwsIotCompletion + */ +@Getter +@Setter +public class AwsIotDeviceCommand extends AwsIotCompletion { + + private static final Logger LOGGER = Logger.getLogger(AwsIotDeviceCommand.class.getName()); + + private final AwsIotDeviceCommandManager commandManager; + private final Command command; + private final String commandId; + + private AWSIotMessage response; + + @Setter(AccessLevel.NONE) + private Boolean requestSent; + + public AwsIotDeviceCommand(AwsIotDeviceCommandManager commandManager, Command command, String commandId, + AWSIotMessage request, long commandTimeout, boolean isAsync) { + super(request, commandTimeout, isAsync); + this.commandManager = commandManager; + this.command = command; + this.commandId = commandId; + this.requestSent = false; + } + + public void put(AbstractAwsIotDevice device) throws AWSIotException { + if (device.isCommandReady(command)) { + _put(device); + } else { + LOGGER.info("Request is pending: " + command.name() + "/" + commandId); + } + } + + public String get(AbstractAwsIotDevice device) throws AWSIotException, AWSIotTimeoutException { + super.get(device.getClient()); + return (response != null) ? response.getStringPayload() : null; + } + + public boolean onReady(AbstractAwsIotDevice device) { + try { + LOGGER.info("Request is resumed: " + command.name() + "/" + commandId); + _put(device); + return true; + } catch (AWSIotException e) { + return false; + } + } + + @Override + public void onSuccess() { + // first callback is for the command ack, which we ignore + if (response == null) { + return; + } else { + request.setPayload(response.getPayload()); + } + + super.onSuccess(); + } + + @Override + public void onFailure() { + super.onFailure(); + } + + @Override + public void onTimeout() { + commandManager.onCommandTimeout(this); + super.onTimeout(); + } + + private void _put(AbstractAwsIotDevice device) throws AWSIotException { + synchronized (this) { + if (requestSent) { + LOGGER.warning("Request was already sent: " + command.name() + "/" + commandId); + return; + } else { + requestSent = true; + } + } + + device.getClient().publish(this, timeout); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceCommandAckListener.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceCommandAckListener.java new file mode 100644 index 00000000000..742106f04d9 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceCommandAckListener.java @@ -0,0 +1,61 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.shadow; + +import java.util.logging.Logger; + +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.AWSIotQos; +import com.amazonaws.services.iot.client.AWSIotTopic; + +/** + * This class extends {@link AWSIotTopic} to provide customized callback + * functions for the subscription requests of the shadow commands. + */ +public class AwsIotDeviceCommandAckListener extends AWSIotTopic { + + private static final Logger LOGGER = Logger.getLogger(AwsIotDeviceCommandAckListener.class.getName()); + + private final AbstractAwsIotDevice device; + + public AwsIotDeviceCommandAckListener(String topic, AWSIotQos qos, AbstractAwsIotDevice device) { + super(topic, qos); + this.device = device; + } + + @Override + public void onMessage(AWSIotMessage message) { + device.onCommandAck(message); + } + + @Override + public void onSuccess() { + device.onSubscriptionAck(topic, true); + } + + @Override + public void onFailure() { + LOGGER.warning("Failed to subscribe to device topic " + topic); + device.onSubscriptionAck(topic, false); + } + + @Override + public void onTimeout() { + LOGGER.warning("Timeout when subscribing to device topic " + topic); + device.onSubscriptionAck(topic, false); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceCommandManager.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceCommandManager.java new file mode 100644 index 00000000000..af374b069d3 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceCommandManager.java @@ -0,0 +1,329 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.shadow; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.amazonaws.services.iot.client.AWSIotDeviceErrorCode; +import com.amazonaws.services.iot.client.AWSIotException; +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.AWSIotTimeoutException; +import com.amazonaws.services.iot.client.core.AwsIotRuntimeException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import lombok.Getter; +import lombok.Setter; + +/** + * This class manages the commands sent to the shadow. It maintains a list of + * pending commands that are yet to be accepted or rejected by the shadow. Upon + * receiving the shadow response for a command, it will notify therefore resume + * the execution of the caller. + */ +@Getter +@Setter +public class AwsIotDeviceCommandManager { + + private static final Logger LOGGER = Logger.getLogger(AwsIotDeviceCommandManager.class.getName()); + + private static final String TOPIC_PREFIX = "$aws/things/?/shadow"; + private static final String COMMAND_ID_FIELD = "clientToken"; + private static final String ERROR_CODE_FIELD = "code"; + private static final String ERROR_MESSAGE_FIELD = "message"; + private static final Map COMMAND_PATHS; + private static final Map COMMAND_ACK_PATHS; + private static final Pattern commandPattern; + private static final Pattern deltaPattern; + + private final ConcurrentMap pendingCommands; + private final AbstractAwsIotDevice device; + private final ObjectMapper objectMapper; + + public static enum Command { + GET, UPDATE, DELETE, DELTA + } + + public static enum CommandAck { + ACCEPTED, REJECTED + } + + static { + COMMAND_PATHS = new HashMap(); + COMMAND_PATHS.put(Command.GET, "/get"); + COMMAND_PATHS.put(Command.UPDATE, "/update"); + COMMAND_PATHS.put(Command.DELETE, "/delete"); + COMMAND_PATHS.put(Command.DELTA, "/update/delta"); + + COMMAND_ACK_PATHS = new HashMap(); + COMMAND_ACK_PATHS.put(CommandAck.ACCEPTED, "/accepted"); + COMMAND_ACK_PATHS.put(CommandAck.REJECTED, "/rejected"); + + commandPattern = Pattern.compile("^\\$aws/things/[^/]+/shadow/(get|update|delete)/(?:accepted|rejected)$"); + deltaPattern = Pattern.compile("^\\$aws/things/[^/]+/shadow/update/delta$"); + } + + public AwsIotDeviceCommandManager(AbstractAwsIotDevice device) { + this.pendingCommands = new ConcurrentHashMap<>(); + this.device = device; + this.objectMapper = new ObjectMapper(); + } + + public String getTopic(Command command, CommandAck ack) { + String topic = TOPIC_PREFIX.replace("?", device.getThingName()); + + if (COMMAND_PATHS.containsKey(command)) { + topic += COMMAND_PATHS.get(command); + } + + if (COMMAND_ACK_PATHS.containsKey(ack)) { + topic += COMMAND_ACK_PATHS.get(ack); + } + + return topic; + } + + public String runCommandSync(Command command, AWSIotMessage request) throws AWSIotException { + try { + return runCommand(command, request, 0, false); + } catch (AWSIotTimeoutException e) { + // We shouldn't get timeout exception because timeout is 0 + throw new AwsIotRuntimeException(e); + } + } + + public String runCommandSync(Command command, AWSIotMessage request, long commandTimeout) throws AWSIotException, + AWSIotTimeoutException { + return runCommand(command, request, commandTimeout, false); + } + + public String runCommand(Command command, AWSIotMessage request, long commandTimeout) throws AWSIotException { + try { + return runCommand(command, request, commandTimeout, true); + } catch (AWSIotTimeoutException e) { + // We shouldn't get timeout exception because it's asynchronous call + throw new AwsIotRuntimeException(e); + } + } + + public String runCommand(Command command, AWSIotMessage request, long commandTimeout, boolean isAsync) + throws AWSIotException, AWSIotTimeoutException { + String commandId = newCommandId(); + appendCommandId(request, commandId); + + request.setTopic(getTopic(command, null)); + AwsIotDeviceCommand deviceCommand = new AwsIotDeviceCommand(this, command, commandId, request, commandTimeout, + isAsync); + + pendingCommands.put(commandId, deviceCommand); + LOGGER.fine("Number of pending commands: " + pendingCommands.size()); + + try { + deviceCommand.put(device); + } catch (AWSIotException e) { + // if exception happens during publish, we remove the command + // from the pending list as we'll never get ack for it. + pendingCommands.remove(commandId); + throw e; + } + + return deviceCommand.get(device); + } + + public void onCommandAck(AWSIotMessage response) { + if (response == null || response.getTopic() == null) { + return; + } + + AwsIotDeviceCommand command = getPendingCommand(response); + if (command == null) { + LOGGER.warning("Unknown command received from topic " + response.getTopic()); + return; + } + + boolean success = response.getTopic().endsWith(COMMAND_ACK_PATHS.get(CommandAck.ACCEPTED)); + if (!success + && (Command.DELETE.equals(command.getCommand()) && AWSIotDeviceErrorCode.NOT_FOUND.equals(command + .getErrorCode()))) { + // Ignore empty document error (NOT_FOUND) for delete command + success = true; + } + + if (success) { + command.setResponse(response); + command.onSuccess(); + } else { + command.onFailure(); + } + } + + public void onCommandTimeout(AwsIotDeviceCommand command) { + pendingCommands.remove(command.getCommandId()); + } + + public void onSubscriptionAck(String topic, boolean success) { + boolean ready = false; + + Command command = getCommandFromTopic(topic); + if (command == null) { + return; + } + + String accepted = getTopic(command, CommandAck.ACCEPTED); + String rejected = getTopic(command, CommandAck.REJECTED); + if (accepted.equals(topic) || rejected.equals(topic)) { + if (success && device.isTopicReady(accepted) && device.isTopicReady(rejected)) { + ready = true; + } + } + + Iterator> it = pendingCommands.entrySet().iterator(); + while (it.hasNext()) { + Entry entry = it.next(); + AwsIotDeviceCommand deviceCommand = entry.getValue(); + + boolean failCommand = false; + if (command.equals(deviceCommand.getCommand())) { + if (ready) { + if (!deviceCommand.onReady(device)) { + failCommand = true; + } + } else if (!success) { + failCommand = true; + } + } + + if (failCommand) { + it.remove(); + deviceCommand.onFailure(); + } + } + } + + public void onDeactivate() { + Iterator> it = pendingCommands.entrySet().iterator(); + while (it.hasNext()) { + Entry entry = it.next(); + it.remove(); + + final AwsIotDeviceCommand deviceCommand = entry.getValue(); + LOGGER.warning("Request was cancelled: " + deviceCommand.getCommand().name() + "/" + + deviceCommand.getCommandId()); + device.getClient().scheduleTask(new Runnable() { + @Override + public void run() { + deviceCommand.onFailure(); + } + }); + } + } + + public boolean isDeltaTopic(String topic) { + if (topic == null) { + return false; + } + + Matcher matcher = deltaPattern.matcher(topic); + return matcher.matches(); + } + + private Command getCommandFromTopic(String topic) { + if (topic == null) { + return null; + } + + Matcher matcher = commandPattern.matcher(topic); + if (matcher.find()) { + String name = matcher.group(1); + return Command.valueOf(name.toUpperCase()); + } + return null; + } + + private void appendCommandId(AWSIotMessage message, String commandId) throws AWSIotException { + String payload = message.getStringPayload(); + if (payload == null) { + payload = "{}"; + } + + try { + JsonNode jsonNode = objectMapper.readTree(payload); + if (!jsonNode.isObject()) { + throw new AWSIotException("Invalid Json string in payload"); + } + ((ObjectNode) jsonNode).put(COMMAND_ID_FIELD, commandId); + + message.setStringPayload(jsonNode.toString()); + } catch (IOException e) { + throw new AWSIotException(e); + } + } + + private AwsIotDeviceCommand getPendingCommand(AWSIotMessage message) { + String payload = message.getStringPayload(); + if (payload == null) { + return null; + } + + try { + JsonNode jsonNode = objectMapper.readTree(payload); + if (!jsonNode.isObject()) { + return null; + } + + JsonNode node = jsonNode.get(COMMAND_ID_FIELD); + if (node == null) { + return null; + } + + String commandId = node.textValue(); + AwsIotDeviceCommand command = pendingCommands.remove(commandId); + if (command == null) { + return null; + } + + node = jsonNode.get(ERROR_CODE_FIELD); + if (node != null) { + command.setErrorCode(AWSIotDeviceErrorCode.valueOf(node.longValue())); + } + + node = jsonNode.get(ERROR_MESSAGE_FIELD); + if (node != null) { + command.setErrorMessage(node.textValue()); + } + + return command; + } catch (IOException e) { + return null; + } + } + + private String newCommandId() { + return UUID.randomUUID().toString(); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceDeltaListener.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceDeltaListener.java new file mode 100644 index 00000000000..c3e3a658b2a --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceDeltaListener.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.shadow; + +import java.io.IOException; +import java.util.logging.Logger; + +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.AWSIotQos; +import com.amazonaws.services.iot.client.AWSIotTopic; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * This class extends {@link AWSIotTopic} to provide a callback function for + * receiving the shadow delta updates. + */ +public class AwsIotDeviceDeltaListener extends AWSIotTopic { + + private static final Logger LOGGER = Logger.getLogger(AwsIotDeviceDeltaListener.class.getName()); + + private final AbstractAwsIotDevice device; + + public AwsIotDeviceDeltaListener(String topic, AWSIotQos qos, AbstractAwsIotDevice device) { + super(topic, qos); + this.device = device; + } + + @Override + public void onMessage(AWSIotMessage message) { + String payload = message.getStringPayload(); + if (payload == null) { + LOGGER.warning("Received empty delta for device " + device.getThingName()); + return; + } + + JsonNode rootNode; + try { + rootNode = device.getJsonObjectMapper().readTree(payload); + if (!rootNode.isObject()) { + throw new IOException(); + } + } catch (IOException e) { + LOGGER.warning("Received invalid delta for device " + device.getThingName()); + return; + } + + if (device.enableVersioning) { + JsonNode node = rootNode.get("version"); + if (node == null) { + LOGGER.warning("Missing version field in delta for device " + device.getThingName()); + return; + } + + long receivedVersion = node.longValue(); + long localVersion = device.getLocalVersion().get(); + if (receivedVersion < localVersion) { + LOGGER.warning("An old version of delta received for " + device.getThingName() + ", local " + + localVersion + ", received " + receivedVersion); + return; + } + + device.getLocalVersion().set(receivedVersion); + LOGGER.info("Local version number updated to " + receivedVersion); + } + + JsonNode node = rootNode.get("state"); + if (node == null) { + LOGGER.warning("Missing state field in delta for device " + device.getThingName()); + return; + } + device.onShadowUpdate(node.toString()); + } + + @Override + public void onSuccess() { + } + + @Override + public void onFailure() { + LOGGER.warning("Failed to subscribe to device topic " + topic); + } + + @Override + public void onTimeout() { + LOGGER.warning("Timeout when subscribing to device topic " + topic); + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceReportMessage.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceReportMessage.java new file mode 100644 index 00000000000..a76db6a6ef0 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceReportMessage.java @@ -0,0 +1,54 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.shadow; + +import java.util.logging.Logger; + +import com.amazonaws.services.iot.client.AWSIotDeviceErrorCode; +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.AWSIotQos; + +public class AwsIotDeviceReportMessage extends AWSIotMessage { + + private static final Logger LOGGER = Logger.getLogger(AwsIotDeviceReportMessage.class.getName()); + + private final AbstractAwsIotDevice device; + private final long reportVersion; + + public AwsIotDeviceReportMessage(String topic, AWSIotQos qos, long reportVersion, String jsonState, + AbstractAwsIotDevice device) { + super(topic, qos, jsonState); + this.device = device; + this.reportVersion = reportVersion; + } + + @Override + public void onSuccess() { + // increment local version only if it hasn't be updated + device.getLocalVersion().compareAndSet(reportVersion, reportVersion + 1); + } + + @Override + public void onFailure() { + if (AWSIotDeviceErrorCode.CONFLICT.equals(errorCode)) { + LOGGER.warning("Device version conflict, restart version synchronization"); + device.startVersionSync(); + } else { + LOGGER.warning("Failed to publish device report: " + errorMessage); + } + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceSyncMessage.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceSyncMessage.java new file mode 100644 index 00000000000..c4dad5a0b09 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotDeviceSyncMessage.java @@ -0,0 +1,66 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.shadow; + +import java.io.IOException; +import java.util.logging.Logger; + +import com.amazonaws.services.iot.client.AWSIotDeviceErrorCode; +import com.amazonaws.services.iot.client.AWSIotMessage; +import com.amazonaws.services.iot.client.AWSIotQos; + +public class AwsIotDeviceSyncMessage extends AWSIotMessage { + + private static final Logger LOGGER = Logger.getLogger(AwsIotDeviceSyncMessage.class.getName()); + + private final AbstractAwsIotDevice device; + + public AwsIotDeviceSyncMessage(String topic, AWSIotQos qos, AbstractAwsIotDevice device) { + super(topic, qos); + this.device = device; + } + + @Override + public void onSuccess() { + if (payload != null) { + try { + long version = AwsIotJsonDeserializer.deserializeVersion(device, getStringPayload()); + if (version > 0) { + LOGGER.info("Received shadow version number: " + version); + + boolean updated = device.getLocalVersion().compareAndSet(-1, version); + if (!updated) { + LOGGER.warning( + "Local version not updated likely because newer version recieved from shadow update"); + } + } + } catch (IOException e) { + LOGGER.warning("Device update error: " + e.getMessage()); + } + } + } + + @Override + public void onFailure() { + if (AWSIotDeviceErrorCode.NOT_FOUND.equals(errorCode)) { + LOGGER.info("No shadow document found, reset local version to 0"); + device.getLocalVersion().set(0); + } else { + LOGGER.warning("Failed to get shadow version: " + errorMessage); + } + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotJsonDeserializer.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotJsonDeserializer.java new file mode 100644 index 00000000000..f1b81eb2b27 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotJsonDeserializer.java @@ -0,0 +1,92 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.shadow; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Iterator; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * This is a customized JSON deserializer for deserializing the delta update + * document from the shadow. + */ +public class AwsIotJsonDeserializer { + + public static void deserialize(AbstractAwsIotDevice device, String jsonState) throws IOException { + ObjectMapper jsonObjectMapper = device.getJsonObjectMapper(); + + JsonNode node = jsonObjectMapper.readTree(jsonState); + if (node == null) { + throw new IOException("Invalid delta update received for " + device.getThingName()); + } + + for (Iterator it = node.fieldNames(); it.hasNext();) { + String property = it.next(); + Field field = device.getUpdatableProperties().get(property); + JsonNode fieldNode = node.get(property); + if (field == null || fieldNode == null) { + continue; + } + + updateDeviceProperty(jsonObjectMapper, fieldNode, device, field); + } + } + + public static long deserializeVersion(AbstractAwsIotDevice device, String jsonState) throws IOException { + ObjectMapper jsonObjectMapper = device.getJsonObjectMapper(); + + JsonNode node = jsonObjectMapper.readTree(jsonState); + if (node == null) { + throw new IOException("Invalid shadow document received for " + device.getThingName()); + } + + JsonNode versionNode = node.get("version"); + if (versionNode == null) { + throw new IOException("Missing version field from shadow document for " + device.getThingName()); + } + + return versionNode.asLong(); + } + + private static void updateDeviceProperty(ObjectMapper jsonObjectMapper, JsonNode node, AbstractAwsIotDevice device, + Field field) throws IOException { + Object value = jsonObjectMapper.treeToValue(node, field.getType()); + invokeSetterMethod(device, field.getName(), field.getType(), value); + } + + private static void invokeSetterMethod(Object target, String name, Class type, Object value) throws IOException { + String setter = "set" + Character.toUpperCase(name.charAt(0)) + name.substring(1); + + Method method; + try { + method = target.getClass().getMethod(setter, type); + } catch (NoSuchMethodException | SecurityException e) { + throw new IllegalArgumentException(e); + } + + try { + method.invoke(target, value); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new IOException(e); + } + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotJsonSerializer.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotJsonSerializer.java new file mode 100644 index 00000000000..37f6cfc768a --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/shadow/AwsIotJsonSerializer.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.shadow; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +/** + * This is a customized JSON serializer for the Jackson databind module. It is + * used for serializing the device properties to be reported to the shadow. + */ +public class AwsIotJsonSerializer extends JsonSerializer { + + @Override + public void serialize(AbstractAwsIotDevice device, JsonGenerator generator, SerializerProvider provider) + throws IOException, JsonProcessingException { + generator.writeStartObject(); + + try { + for (String property : device.getReportedProperties().keySet()) { + Field field = device.getReportedProperties().get(property); + + Object value = invokeGetterMethod(device, field); + generator.writeObjectField(property, value); + } + } catch (IllegalArgumentException e) { + throw new IOException(e); + } + + generator.writeEndObject(); + } + + private static Object invokeGetterMethod(Object target, Field field) throws IOException { + String fieldName = Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1); + String getter = "get" + fieldName; + + Method method; + try { + method = target.getClass().getMethod(getter); + } catch (NoSuchMethodException | SecurityException e) { + if (e instanceof NoSuchMethodException && boolean.class.equals(field.getType())) { + getter = "is" + fieldName; + try { + method = target.getClass().getMethod(getter); + } catch (NoSuchMethodException | SecurityException ie) { + throw new IllegalArgumentException(ie); + } + } else { + throw new IllegalArgumentException(e); + } + } + + Object value; + try { + value = method.invoke(target); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new IOException(e); + } + return value; + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/util/AwsIotTlsSocketFactory.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/util/AwsIotTlsSocketFactory.java new file mode 100644 index 00000000000..8582a9ade64 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/util/AwsIotTlsSocketFactory.java @@ -0,0 +1,124 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.util; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStore; + +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import com.amazonaws.services.iot.client.AWSIotException; + +/** + * This class extends {@link SSLSocketFactory} to enforce TLS v1.2 to be used + * for SSL sockets created by the library. + */ +public class AwsIotTlsSocketFactory extends SSLSocketFactory { + private static final String TLS_V_1_2 = "TLSv1.2"; + + /** + * SSL Socket Factory A SSL socket factory is created and passed into this + * class which decorates it to enable TLS 1.2 when sockets are created. + */ + private final SSLSocketFactory sslSocketFactory; + + public AwsIotTlsSocketFactory(KeyStore keyStore, String keyPassword) throws AWSIotException { + try { + SSLContext context = SSLContext.getInstance(TLS_V_1_2); + + KeyManagerFactory managerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + managerFactory.init(keyStore, keyPassword.toCharArray()); + context.init(managerFactory.getKeyManagers(), null, null); + + sslSocketFactory = context.getSocketFactory(); + } catch (NoSuchAlgorithmException | KeyStoreException | UnrecoverableKeyException | KeyManagementException e) { + throw new AWSIotException(e); + } + } + + public AwsIotTlsSocketFactory(SSLSocketFactory sslSocketFactory) { + this.sslSocketFactory = sslSocketFactory; + } + + @Override + public String[] getDefaultCipherSuites() { + return sslSocketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return sslSocketFactory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException { + return ensureTls(sslSocketFactory.createSocket()); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + return ensureTls(sslSocketFactory.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + return ensureTls(sslSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException, UnknownHostException { + return ensureTls(sslSocketFactory.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return ensureTls(sslSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) + throws IOException { + return ensureTls(sslSocketFactory.createSocket(address, port, localAddress, localPort)); + } + + /** + * Enable TLS 1.2 on any socket created by the underlying SSL Socket + * Factory. + * + * @param socket + * newly created socket which may not have TLS 1.2 enabled. + * @return TLS 1.2 enabled socket. + */ + private Socket ensureTls(Socket socket) { + if (socket != null && (socket instanceof SSLSocket)) { + ((SSLSocket) socket).setEnabledProtocols(new String[] { TLS_V_1_2 }); + } + return socket; + } + +} diff --git a/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/util/AwsIotWebSocketUrlSigner.java b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/util/AwsIotWebSocketUrlSigner.java new file mode 100644 index 00000000000..e5f12df8b37 --- /dev/null +++ b/aws-iot-device-sdk-java/src/main/java/com/amazonaws/services/iot/client/util/AwsIotWebSocketUrlSigner.java @@ -0,0 +1,320 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.iot.client.util; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.security.MessageDigest; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import com.amazonaws.services.iot.client.AWSIotException; + +/** + * The AWSIotWebSocketUrlSigner class creates the SigV4 signature and builds a + * connection URL to be used with the Paho MQTT client. + */ +public class AwsIotWebSocketUrlSigner { + + /** Constant defining the algorithm use for hash calculation. */ + private static final String HASH_ALGORITHM = "SHA-256"; + /** Constant defining the algorithm use for MAC calculation. */ + private static final String HMAC_ALGORITHM = "HmacSHA256"; + /** Constant defining the algorithm specifier in SigV4 parameters. */ + private static final String ALGORITHM = "AWS4-HMAC-SHA256"; + /** Constant defining the key prefix string in SigV4 parameters. */ + private static final String KEY_PREFIX = "AWS4"; + /** Constant defining the terminator string in SigV4 parameters. */ + private static final String TERMINATOR = "aws4_request"; + /** Short date format pattern used in SigV4 parameters. */ + private static final String DATE_PATTERN = "yyyyMMdd"; + /** ISO 8601 date format pattern used in SigV4 signature parameters. */ + private static final String TIME_PATTERN = "yyyyMMdd'T'HHmmss'Z'"; + /** Default timezone used for converting signing date. */ + private static final TimeZone TIME_ZONE = TimeZone.getTimeZone("UTC"); + /** Default charset used for URL encoding. */ + private static final String UTF8 = "UTF-8"; + /** Constant defining the HTTP method for the WebSocket connection. */ + private static final String METHOD = "GET"; + /** URI for WebSocket endpoint when doing initial HTTP operation. */ + private static final String CANONICAL_URI = "/mqtt"; + /** endpoint pattern used for validation and extracting region. */ + private static final Pattern endpointPattern = Pattern.compile("iot\\.([\\w-]+)\\.amazonaws\\.com(\\:\\d+)?$"); + + /** + * Given the region and service name provided to the client, the endpoint + * and the current time return a signed connection URL to be used when + * connecting via WebSocket to AWS IoT. + * + * @param endpoint + * service endpoint with or without customer specific URL prefix. + * @param awsAccessKeyId + * AWS access key ID used in SigV4 signature algorithm. + * @param awsSecretAccessKey + * AWS secret access key used in SigV4 signature algorithm. + * @param sessionToken + * Session token for temporary credentials. + * @param signingDate + * time value to be used in SigV4 calculations. System current + * time will be used if null. + * @return a URL with SigV4 signature formatted to be used with AWS IoT. + * @throws AWSIotException + * Exception thrown when signed URL can be generated with given + * information. + */ + public static String getSignedUrl(String endpoint, String awsAccessKeyId, String awsSecretAccessKey, + String sessionToken, final Date signingDate) throws AWSIotException { + if (endpoint == null || awsAccessKeyId == null || awsSecretAccessKey == null) { + throw new IllegalArgumentException("Missing required data for signing"); + } + + endpoint = endpoint.trim().toLowerCase(); + String regionName = getRegionFromEndpoint(endpoint); + if (regionName == null) { + throw new IllegalArgumentException("Invalid endpoint provided"); + } + + String serviceName = "iotdata"; + awsAccessKeyId = awsAccessKeyId.trim(); + awsSecretAccessKey = awsSecretAccessKey.trim(); + + Date dateToUse = signingDate; + if (dateToUse == null) { + dateToUse = new Date(); + } + + // SigV4 canonical string uses time in two formats + String amzDate = getAmzDate(dateToUse); + String dateStamp = getDateStamp(dateToUse); + // Credential scoped to date and region + String credentialScope = dateStamp + "/" + regionName + "/" + serviceName + "/aws4_request"; + // Now build the canonical string + StringBuilder canonicalQueryStringBuilder = new StringBuilder(); + canonicalQueryStringBuilder.append("X-Amz-Algorithm=").append(ALGORITHM); + canonicalQueryStringBuilder.append("&X-Amz-Credential="); + try { + canonicalQueryStringBuilder.append(URLEncoder.encode(awsAccessKeyId + "/" + credentialScope, UTF8)); + } catch (UnsupportedEncodingException e) { + throw new AWSIotException("Error encoding URL when building WebSocket URL"); + } + canonicalQueryStringBuilder.append("&X-Amz-Date=").append(amzDate); + canonicalQueryStringBuilder.append("&X-Amz-SignedHeaders=host"); + + // headers and payload for the signing request + // not used in an WebSocket URL, but encoded into the signature string + String canonicalHeaders = "host:" + endpoint + "\n"; + String payloadHash = stringToHex(hash("")); + + // The request to sign includes the HTTP method, path, query string, + // headers and payload + String canonicalRequest = METHOD + "\n" + CANONICAL_URI + "\n" + canonicalQueryStringBuilder.toString() + "\n" + + canonicalHeaders + "\nhost\n" + payloadHash; + + // Create a string to sign, generate a signing key... + String stringToSign = ALGORITHM + "\n" + amzDate + "\n" + credentialScope + "\n" + + stringToHex(hash(canonicalRequest)); + byte[] signingKey = getSigningKey(dateStamp, regionName, serviceName, awsSecretAccessKey); + // ...and sign the string. + byte[] signatureBytes; + try { + signatureBytes = sign(stringToSign.getBytes(UTF8), signingKey); + } catch (UnsupportedEncodingException e) { + throw new AWSIotException("Error encoding URL when generating the signature bytes"); + } + String signature = stringToHex(signatureBytes); + + // Add the signature to the query string. + canonicalQueryStringBuilder.append("&X-Amz-Signature="); + canonicalQueryStringBuilder.append(signature); + + // Now build the URL. + String requestUrl = "wss://" + endpoint + CANONICAL_URI + "?" + canonicalQueryStringBuilder.toString(); + + // If there are session credentials (from an STS server, AssumeRole, or + // Amazon Cognito), + // append the session token to the end of the URL string after signing. + if (sessionToken != null) { + sessionToken = sessionToken.trim(); + try { + sessionToken = URLEncoder.encode(sessionToken, UTF8); + } catch (UnsupportedEncodingException e) { + throw new AWSIotException("Error encoding URL when appending session token to URL"); + } + requestUrl += "&X-Amz-Security-Token=" + sessionToken; + } + + return requestUrl; + } + + private static String getRegionFromEndpoint(String endpoint) { + Matcher matcher = endpointPattern.matcher(endpoint); + if (matcher.find()) { + return matcher.group(1); + } + + return null; + } + + /** + * Converts byte data to a Hex-encoded string. + * + * @param data + * data to hex encode. + * @return hex-encoded string. + */ + private static String stringToHex(final byte[] data) { + StringBuilder sb = new StringBuilder(data.length * 2); + for (int i = 0; i < data.length; i++) { + String hex = Integer.toHexString(data[i]); + if (hex.length() == 1) { + // Append leading zero. + sb.append("0"); + } else if (hex.length() == 8) { + // Remove ff prefix from negative numbers. + hex = hex.substring(6); + } + sb.append(hex); + } + return sb.toString().toLowerCase(); + } + + /** + * The SigV4 signing key is made up by consecutively hashing a number of + * unique pieces of data. + * + * @param dateStamp + * the current date in short date format. + * @param regionName + * AWS region name. + * @param serviceName + * service name for IoT service. + * @param credentials + * AWS credential set to be used in signing. + * @return byte array containing the SigV4 signing key. + * @throws AWSIotException + */ + private static byte[] getSigningKey(String dateStamp, String regionName, String serviceName, + String awsSecretAccessKey) throws AWSIotException { + // AWS4 uses a series of derived keys, formed by hashing different + // pieces of data + byte[] signingSecret; + try { + signingSecret = (KEY_PREFIX + awsSecretAccessKey).getBytes(UTF8); + } catch (UnsupportedEncodingException e) { + throw new AWSIotException("Error encoding URL when generating the signing key"); + } + byte[] signingDate = sign(dateStamp, signingSecret); + byte[] signingRegion = sign(regionName, signingDate); + byte[] signingService = sign(serviceName, signingRegion); + return sign(TERMINATOR, signingService); + } + + /** + * Given the input epoch time returns a String of the proper format for the + * ISO 8601 date + time in SigV4 parameters. + * + * @param date + * desired date. + * @return date formatted string in ISO 8601 date + time format. + */ + private static String getAmzDate(final Date date) { + SimpleDateFormat fomatter = new SimpleDateFormat(TIME_PATTERN); + fomatter.setTimeZone(TIME_ZONE); + return fomatter.format(date); + } + + /** + * Given the input epoch time returns a String of the proper format for the + * short date in SigV4 parameters. + * + * @param date + * desired date. + * @return date formatted string in short date format. + */ + private static String getDateStamp(final Date date) { + SimpleDateFormat fomatter = new SimpleDateFormat(DATE_PATTERN); + fomatter.setTimeZone(TIME_ZONE); + return fomatter.format(date); + } + + /** + * Hashes the string contents (assumed to be UTF-8) using the SHA-256 + * algorithm. + * + * @param text + * The string to hash. + * @return The hashed bytes from the specified string. + * @throws AmazonClientException + * If the hash cannot be computed. + */ + private static byte[] hash(String text) throws AWSIotException { + try { + MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM); + md.update(text.getBytes(UTF8)); + return md.digest(); + } catch (Exception e) { + throw new AWSIotException("Unable to compute hash while signing request: " + e.getMessage()); + } + } + + /** + * Sign the given string with the key provided. + * + * @param stringData + * String to be signed. + * @param key + * the key for signing. + * @return a byte array containing the signed string. + * @throws AmazonClientException + * in the case of a signature error. + */ + private static byte[] sign(String stringData, final byte[] key) throws AWSIotException { + try { + byte[] data = stringData.getBytes(UTF8); + return sign(data, key); + } catch (Exception e) { + throw new AWSIotException("Unable to calculate a request signature: " + e.getMessage()); + } + } + + /** + * Sign the given data with the key provided. + * + * @param data + * byte buffer of data to be signed. + * @param key + * the key for signing. + * @return a byte array containing the signed string. + * @throws AmazonClientException + * in the case of a signature error. + */ + private static byte[] sign(byte[] data, final byte[] key) throws AWSIotException { + try { + Mac mac = Mac.getInstance(HMAC_ALGORITHM); + mac.init(new SecretKeySpec(key, HMAC_ALGORITHM)); + return mac.doFinal(data); + } catch (Exception e) { + throw new AWSIotException("Unable to calculate a request signature: " + e.getMessage()); + } + } + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000000..b037e9bb043 --- /dev/null +++ b/pom.xml @@ -0,0 +1,82 @@ + + 4.0.0 + com.amazonaws + aws-iot-device-sdk-java-pom + 1.0.0 + pom + AWS IoT Device SDK for Java + The AWS IoT Device SDK for Java provides Java APIs for devices to connect to AWS IoT service using the MQTT protocol. The SDK also provides support for AWS IoT specific features, such as Thing Shadow and Thing Shadow abstraction. + https://aws.amazon.com/iot/sdk + + + Apache License, Version 2.0 + https://aws.amazon.com/apache2.0 + repo + + + + + amazonwebservices + Amazon Web Services + https://aws.amazon.com + + developer + + + + + https://github.com/aws/aws-iot-device-sdk-java.git + + + aws-iot-device-sdk-java + aws-iot-device-sdk-java-samples + + + UTF-8 + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.3 + true + + ossrh + https://oss.sonatype.org/ + false + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + 1.7 + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + +