├── .github └── workflows │ ├── build.yml │ ├── projects.yml │ └── release.yml ├── .gitignore ├── .idea ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── encodings.xml ├── jarRepositories.xml └── misc.xml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── pom.xml ├── src ├── main │ └── java │ │ └── io │ │ └── webthings │ │ └── webthing │ │ ├── Action.java │ │ ├── Event.java │ │ ├── Property.java │ │ ├── Thing.java │ │ ├── Utils.java │ │ ├── Value.java │ │ ├── WebThingServer.java │ │ ├── errors │ │ └── PropertyError.java │ │ └── example │ │ ├── MultipleThings.java │ │ └── SingleThing.java └── test │ └── java │ └── io │ └── webthings │ └── webthing │ ├── ThingTest.java │ ├── ValueTest.java │ └── example │ └── SingleThingTest.java └── test.sh /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | java-version: [ 17 | '8.0.x', 18 | '11.0.x', 19 | '17.0.x', 20 | '21.0.x', 21 | ] 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions/setup-python@v2 25 | with: 26 | python-version: '3.9' 27 | - uses: actions/setup-java@v1 28 | with: 29 | java-version: ${{ matrix.java-version }} 30 | architecture: x64 31 | - name: Run integration tests 32 | run: | 33 | ./test.sh 34 | -------------------------------------------------------------------------------- /.github/workflows/projects.yml: -------------------------------------------------------------------------------- 1 | name: Add new issues to the specified project column 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | add-new-issues-to-project-column: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: add-new-issues-to-organization-based-project-column 12 | uses: docker://takanabe/github-actions-automate-projects:v0.0.1 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.CI_TOKEN }} 15 | GITHUB_PROJECT_URL: https://github.com/orgs/WebThingsIO/projects/4 16 | GITHUB_PROJECT_COLUMN_NAME: To do 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Install Java and Maven 14 | uses: actions/setup-java@v1 15 | with: 16 | java-version: 8 17 | server-id: ossrh 18 | server-username: MAVEN_USERNAME 19 | server-password: MAVEN_CENTRAL_TOKEN 20 | gpg-private-key: ${{ secrets.GPG_KEY }} 21 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 22 | - name: Set release version 23 | run: echo "RELEASE_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV 24 | - name: Create Release 25 | id: create_release 26 | uses: actions/create-release@v1.0.0 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | with: 30 | tag_name: ${{ github.ref }} 31 | release_name: Release ${{ env.RELEASE_VERSION }} 32 | draft: false 33 | prerelease: false 34 | - name: Publish to Apache Maven Central 35 | run: mvn --no-transfer-progress deploy --activate-profiles release 36 | env: 37 | MAVEN_USERNAME: ${{ secrets.NEXUS_USERNAME }} 38 | MAVEN_CENTRAL_TOKEN: ${{ secrets.NEXUS_PASSWORD }} 39 | MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.swp 3 | .DS_Store 4 | /.idea/artifacts/ 5 | /.idea/libraries 6 | /.idea/uiDesigner.xml 7 | /.idea/vcs.xml 8 | /.idea/workspace.xml 9 | /out/ 10 | /target/ 11 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | webthing -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 31 | 271 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # webthing Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [0.14.0] - 2021-01-05 6 | ### Added 7 | - Parameter to disable host validation in server. 8 | 9 | ## [0.13.0] - 2020-09-23 10 | ### Changed 11 | - Update author and URLs to indicate new project home. 12 | - Changed package name from `org.mozilla.iot` to `io.webthings`. 13 | 14 | ## [0.12.3] - 2020-06-18 15 | ### Changed 16 | - mDNS record now indicates TLS support. 17 | 18 | ## [0.12.2] - 2020-05-04 19 | ### Changed 20 | - Invalid POST requests to action resources now generate an error status. 21 | 22 | ## [0.12.1] - 2020-03-27 23 | ### Added 24 | - Support OPTIONS requests to allow for CORS. 25 | 26 | ## [0.12.0] - 2019-07-12 27 | ### Changed 28 | - Things now use `title` rather than `name`. 29 | - Things now require a unique ID in the form of a URI. 30 | ### Added 31 | - Ability to set a base URL path on server. 32 | - Support for `id`, `base`, `security`, and `securityDefinitions` keys in thing description. 33 | 34 | ## [0.11.0] - 2019-01-16 35 | ### Changed 36 | - WebThingServer constructor can now take a list of additional API routes. 37 | ### Fixed 38 | - Properties could not include a custom `links` array at initialization. 39 | 40 | ## [0.10.0] - 2018-11-30 41 | ### Changed 42 | - Property, Action, and Event description now use `links` rather than `href`. - [Spec PR](https://github.com/WebThingsIO/wot/pull/119) 43 | 44 | [Unreleased]: https://github.com/WebThingsIO/webthing-java/compare/v0.14.0...HEAD 45 | [0.14.0]: https://github.com/WebThingsIO/webthing-java/compare/v0.13.0...v0.14.0 46 | [0.13.0]: https://github.com/WebThingsIO/webthing-java/compare/v0.12.3...v0.13.0 47 | [0.12.3]: https://github.com/WebThingsIO/webthing-java/compare/v0.12.2...v0.12.3 48 | [0.12.2]: https://github.com/WebThingsIO/webthing-java/compare/v0.12.1...v0.12.2 49 | [0.12.1]: https://github.com/WebThingsIO/webthing-java/compare/v0.12.0...v0.12.1 50 | [0.12.0]: https://github.com/WebThingsIO/webthing-java/compare/v0.11.0...v0.12.0 51 | [0.11.0]: https://github.com/WebThingsIO/webthing-java/compare/v0.10.0...v0.11.0 52 | [0.10.0]: https://github.com/WebThingsIO/webthing-java/compare/v0.9.1...v0.10.0 53 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webthing 2 | 3 | [![Build Status](https://github.com/WebThingsIO/webthing-java/workflows/Java%20package/badge.svg)](https://github.com/WebThingsIO/webthing-java/workflows/Java%20package) 4 | [![Maven](https://img.shields.io/maven-central/v/io.webthings/webthing.svg)](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22io.webthings%22%20AND%20a%3A%22webthing%22) 5 | [![license](https://img.shields.io/badge/license-MPL--2.0-blue.svg)](LICENSE) 6 | 7 | Implementation of an HTTP [Web Thing](https://webthings.io/api). 8 | 9 | # Using 10 | 11 | ## Maven 12 | 13 | Add the following dependency to your project: 14 | 15 | ```xml 16 | 17 | 18 | io.webthings 19 | webthing 20 | 0.15.0 21 | 22 | 23 | ``` 24 | 25 | ## Gradle 26 | 27 | Add the following dependency to your project: 28 | 29 | ```gradle 30 | dependencies { 31 | runtime( 32 | [group: 'io.webthings', name: 'webthing', version: '0.15.0'], 33 | ) 34 | } 35 | ``` 36 | 37 | ## Android Studio 38 | 39 | - Open File → Project Structure 40 | - Select the module you want to add this as a dependency to 41 | - Go to the "Dependencies" tab 42 | - Click green "+" button 43 | - Select "Library dependency" 44 | - Enter `io.webthings:webthing` in the search bar and search 45 | - Select the package in the result and confirm with "OK" 46 | - Click "OK" in the Project Structure dialog 47 | 48 | # Example 49 | 50 | In this example we will set up a dimmable light and a humidity sensor (both using fake data, of course). Both working examples can be found in [here](https://github.com/WebThingsIO/webthing-java/tree/master/src/main/java/io/webthings/webthing/example). 51 | 52 | ## Dimmable Light 53 | 54 | Imagine you have a dimmable light that you want to expose via the web of things API. The light can be turned on/off and the brightness can be set from 0% to 100%. Besides the name, description, and type, a [`Light`](https://webthings.io/schemas/#Light) is required to expose two properties: 55 | * `on`: the state of the light, whether it is turned on or off 56 | * Setting this property via a `PUT {"on": true/false}` call to the REST API toggles the light. 57 | * `brightness`: the brightness level of the light from 0-100% 58 | * Setting this property via a PUT call to the REST API sets the brightness level of this light. 59 | 60 | First we create a new Thing: 61 | 62 | ```java 63 | Thing light = new Thing("urn:dev:ops:my-lamp-1234", 64 | "My Lamp", 65 | new JSONArray(Arrays.asList("OnOffSwitch", "Light")), 66 | "A web connected lamp"); 67 | ``` 68 | 69 | Now we can add the required properties. 70 | 71 | The **`on`** property reports and sets the on/off state of the light. For this, we need to have a `Value` object which holds the actual state and also a method to turn the light on/off. For our purposes, we just want to log the new state if the light is switched on/off. 72 | 73 | ```java 74 | JSONObject onDescription = new JSONObject(); 75 | onDescription.put("@type", "OnOffProperty"); 76 | onDescription.put("title", "On/Off"); 77 | onDescription.put("type", "boolean"); 78 | onDescription.put("description", "Whether the lamp is turned on"); 79 | 80 | Value on = new Value<>(true, 81 | // Here, you could send a signal to 82 | // the GPIO that switches the lamp 83 | // off 84 | v -> System.out.printf( 85 | "On-State is now %s\n", 86 | v)); 87 | 88 | light.addProperty(new Property(light, "on", on, onDescription)); 89 | ``` 90 | 91 | The **`brightness`** property reports the brightness level of the light and sets the level. Like before, instead of actually setting the level of a light, we just log the level. 92 | 93 | ```java 94 | JSONObject brightnessDescription = new JSONObject(); 95 | brightnessDescription.put("@type", "BrightnessProperty"); 96 | brightnessDescription.put("title", "Brightness"); 97 | brightnessDescription.put("type", "number"); 98 | brightnessDescription.put("description", 99 | "The level of light from 0-100"); 100 | brightnessDescription.put("minimum", 0); 101 | brightnessDescription.put("maximum", 100); 102 | brightnessDescription.put("unit", "percent"); 103 | 104 | Value level = new Value<>(0.0, 105 | // Here, you could send a signal 106 | // to the GPIO that controls the 107 | // brightness 108 | l -> System.out.printf( 109 | "Brightness is now %s\n", 110 | l)); 111 | 112 | light.addProperty(new Property(light, "level", level, brightnessDescription)); 113 | ``` 114 | 115 | Now we can add our newly created thing to the server and start it: 116 | 117 | ```java 118 | try { 119 | // If adding more than one thing, use MultipleThings() with a name. 120 | // In the single thing case, the thing's name will be broadcast. 121 | WebThingServer server = new WebThingServer(new SingleThing(light), 8888); 122 | 123 | Runtime.getRuntime().addShutdownHook(new Thread() { 124 | public void run() { 125 | server.stop(); 126 | } 127 | }); 128 | 129 | server.start(false); 130 | } catch (IOException e) { 131 | System.out.println(e); 132 | System.exit(1); 133 | } 134 | ``` 135 | 136 | This will start the server, making the light available via the WoT REST API and announcing it as a discoverable resource on your local network via mDNS. 137 | 138 | ## Sensor 139 | 140 | Let's now also connect a humidity sensor to the server we set up for our light. 141 | 142 | A [`MultiLevelSensor`](https://webthings.io/schemas/#MultiLevelSensor) (a sensor that returns a level instead of just on/off) has one required property (besides the name, type, and optional description): **`level`**. We want to monitor this property and get notified if the value changes. 143 | 144 | First we create a new Thing: 145 | 146 | ```java 147 | Thing sensor = new Thing("urn:dev:ops:my-humidity-sensor-1234", 148 | "My Humidity Sensor", 149 | new JSONArray(Arrays.asList("MultiLevelSensor")), 150 | "A web connected humidity sensor"); 151 | ``` 152 | 153 | Then we create and add the appropriate property: 154 | * `level`: tells us what the sensor is actually reading 155 | * Contrary to the light, the value cannot be set via an API call, as it wouldn't make much sense, to SET what a sensor is reading. Therefore, we are creating a *readOnly* property. 156 | 157 | ```java 158 | JSONObject levelDescription = new JSONObject(); 159 | levelDescription.put("@type", "LevelProperty"); 160 | levelDescription.put("title", "Humidity"); 161 | levelDescription.put("type", "number"); 162 | levelDescription.put("description", "The current humidity in %"); 163 | levelDescription.put("minimum", 0); 164 | levelDescription.put("maximum", 100); 165 | levelDescription.put("unit", "percent"); 166 | levelDescription.put("readOnly", true); 167 | 168 | this.level = new Value<>(0.0); 169 | 170 | sensor.addProperty(new Property(sensor, "level", level, levelDescription)); 171 | ``` 172 | 173 | Now we have a sensor that constantly reports 0%. To make it usable, we need a thread or some kind of input when the sensor has a new reading available. For this purpose we start a thread that queries the physical sensor every few seconds. For our purposes, it just calls a fake method. 174 | 175 | ```java 176 | // Start a thread that polls the sensor reading every 3 seconds 177 | new Thread(()->{ 178 | while(true){ 179 | try { 180 | Thread.sleep(3000); 181 | // Updates the underlying value, which in turn notifies all 182 | // listeners 183 | this.level.notifyOfExternalUpdate(readFromGPIO()); 184 | } catch (InterruptedException e) { 185 | throw new IllegalStateException(e); 186 | } 187 | } 188 | }).start(); 189 | ``` 190 | 191 | This will update our `Value` object with the sensor readings via the `this.level.notifyOfExternalUpdate(readFromGPIO());` call. The `Value` object now notifies the property and the thing that the value has changed, which in turn notifies all websocket listeners. 192 | 193 | # Adding to Gateway 194 | 195 | To add your web thing to the WebThings Gateway, install the "Web Thing" add-on and follow the instructions [here](https://github.com/WebThingsIO/thing-url-adapter#readme). 196 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.webthings 8 | webthing 9 | 0.15.0 10 | 11 | WebThing 12 | Implementation of an HTTP Web Thing. 13 | https://github.com/WebThingsIO/webthing-java 14 | 15 | 16 | 17 | Mozilla Public License, Version 2.0 (MPL 2.0) 18 | https://www.mozilla.org/en-US/MPL/2.0/ 19 | repo 20 | 21 | 22 | 23 | 24 | scm:git:https://github.com/WebThingsIO/webthing-java.git 25 | 26 | 27 | scm:git:https://github.com/WebThingsIO/webthing-java.git 28 | 29 | https://github.com/WebThingsIO/webthing-java 30 | HEAD 31 | 32 | 33 | 34 | WebThingsIO 35 | https://webthings.io 36 | 37 | 38 | 39 | 40 | mrstegeman 41 | Michael Stegeman 42 | https://github.com/mrstegeman 43 | 44 | Administrator 45 | Developer 46 | 47 | 48 | https://avatars1.githubusercontent.com/u/457381 49 | 50 | 51 | 52 | Timmeey 53 | Tim Hinkes 54 | https://github.com/Timmeey 55 | 56 | Developer 57 | 58 | 59 | https://avatars2.githubusercontent.com/u/1174542 60 | 61 | 62 | 63 | 64 | 65 | 2018 66 | 67 | 68 | github 69 | https://github.com/WebThingsIO/webthing-java/issues 70 | 71 | 72 | 73 | 74 | org.json 75 | json 76 | 20240303 77 | 78 | 79 | org.nanohttpd 80 | nanohttpd 81 | 2.3.1 82 | 83 | 84 | org.nanohttpd 85 | nanohttpd-nanolets 86 | 2.3.1 87 | 88 | 89 | org.nanohttpd 90 | nanohttpd-websocket 91 | 2.3.1 92 | 93 | 94 | org.jmdns 95 | jmdns 96 | 3.5.9 97 | 98 | 99 | com.github.everit-org.json-schema 100 | org.everit.json.schema 101 | 1.12.1 102 | 103 | 104 | junit 105 | junit 106 | 4.13.1 107 | test 108 | 109 | 110 | 111 | 112 | 113 | jitpack.io 114 | https://jitpack.io 115 | 116 | 117 | 118 | 119 | 120 | 121 | org.apache.maven.plugins 122 | maven-compiler-plugin 123 | 3.8.1 124 | 125 | 8 126 | 8 127 | 128 | 129 | 130 | org.apache.maven.plugins 131 | maven-source-plugin 132 | 3.2.1 133 | 134 | 135 | attach-sources 136 | 137 | jar 138 | 139 | 140 | 141 | 142 | 143 | org.apache.maven.plugins 144 | maven-javadoc-plugin 145 | 3.2.0 146 | 147 | 8 148 | 8 149 | 150 | 151 | 152 | attach-javadoc 153 | 154 | jar 155 | 156 | 157 | 158 | 159 | 160 | org.apache.maven.plugins 161 | maven-assembly-plugin 162 | 3.2.0 163 | 164 | 165 | 166 | 167 | io.webthings.webthing.example.SingleThing 168 | 169 | 170 | 171 | 172 | jar-with-dependencies 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | release 182 | 183 | 184 | 185 | org.apache.maven.plugins 186 | maven-gpg-plugin 187 | 1.6 188 | 189 | 190 | sign-artifacts 191 | verify 192 | 193 | sign 194 | 195 | 196 | 197 | 198 | --pinentry-mode 199 | loopback 200 | 201 | 202 | 203 | 204 | 205 | 206 | org.sonatype.plugins 207 | nexus-staging-maven-plugin 208 | 1.6.8 209 | true 210 | 211 | ossrh 212 | https://oss.sonatype.org/ 213 | true 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | ossrh 224 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 225 | 226 | 227 | 228 | ossrh 229 | https://oss.sonatype.org/content/repositories/snapshots 230 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /src/main/java/io/webthings/webthing/Action.java: -------------------------------------------------------------------------------- 1 | /** 2 | * High-level Action base class implementation. 3 | */ 4 | package io.webthings.webthing; 5 | 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | /** 10 | * An Action represents an individual action on a thing. 11 | */ 12 | public class Action { 13 | private final String id; 14 | private final Thing thing; 15 | private final String name; 16 | private final JSONObject input; 17 | private String hrefPrefix; 18 | private final String href; 19 | private String status; 20 | private final String timeRequested; 21 | private String timeCompleted; 22 | 23 | /** 24 | * Initialize the object. 25 | * 26 | * @param id ID of this action 27 | * @param thing Thing this action belongs to 28 | * @param name Name of the action 29 | */ 30 | public Action(String id, Thing thing, String name) { 31 | this(id, thing, name, null); 32 | } 33 | 34 | /** 35 | * Initialize the object. 36 | * 37 | * @param id ID of this action 38 | * @param thing Thing this action belongs to 39 | * @param name Name of the action 40 | * @param input Any action inputs 41 | */ 42 | public Action(String id, Thing thing, String name, JSONObject input) { 43 | this.id = id; 44 | this.thing = thing; 45 | this.name = name; 46 | this.input = input; 47 | this.hrefPrefix = ""; 48 | this.href = String.format("/actions/%s/%s", this.name, this.id); 49 | this.status = "created"; 50 | this.timeRequested = Utils.timestamp(); 51 | } 52 | 53 | /** 54 | * Get the action description. 55 | * 56 | * @return Description of the action as a JSONObject. 57 | */ 58 | public JSONObject asActionDescription() { 59 | JSONObject obj = new JSONObject(); 60 | JSONObject inner = new JSONObject(); 61 | try { 62 | inner.put("href", this.hrefPrefix + this.href); 63 | inner.put("timeRequested", this.timeRequested); 64 | inner.put("status", this.status); 65 | 66 | if (this.input != null) { 67 | inner.put("input", this.input); 68 | } 69 | 70 | if (this.timeCompleted != null) { 71 | inner.put("timeCompleted", this.timeCompleted); 72 | } 73 | 74 | obj.put(this.name, inner); 75 | return obj; 76 | } catch (JSONException e) { 77 | return null; 78 | } 79 | } 80 | 81 | /** 82 | * Set the prefix of any hrefs associated with this action. 83 | * 84 | * @param prefix The prefix 85 | */ 86 | public void setHrefPrefix(String prefix) { 87 | this.hrefPrefix = prefix; 88 | } 89 | 90 | /** 91 | * Get this action's ID. 92 | * 93 | * @return The ID. 94 | */ 95 | public String getId() { 96 | return this.id; 97 | } 98 | 99 | /** 100 | * Get this action's name. 101 | * 102 | * @return The name. 103 | */ 104 | public String getName() { 105 | return this.name; 106 | } 107 | 108 | /** 109 | * Get this action's href. 110 | * 111 | * @return The href. 112 | */ 113 | public String getHref() { 114 | return this.hrefPrefix + this.href; 115 | } 116 | 117 | /** 118 | * Get this action's status. 119 | * 120 | * @return The status. 121 | */ 122 | public String getStatus() { 123 | return this.status; 124 | } 125 | 126 | /** 127 | * Get the thing associated with this action. 128 | * 129 | * @return The thing. 130 | */ 131 | public Thing getThing() { 132 | return this.thing; 133 | } 134 | 135 | /** 136 | * Get the time the action was requested. 137 | * 138 | * @return The time. 139 | */ 140 | public String getTimeRequested() { 141 | return this.timeRequested; 142 | } 143 | 144 | /** 145 | * Get the time the action was completed. 146 | * 147 | * @return The time. 148 | */ 149 | public String getTimeCompleted() { 150 | return this.timeCompleted; 151 | } 152 | 153 | /** 154 | * Get the inputs for this action. 155 | * 156 | * @return The inputs. 157 | */ 158 | public JSONObject getInput() { 159 | return input; 160 | } 161 | 162 | /** 163 | * Start performing the action. 164 | */ 165 | public void start() { 166 | this.status = "pending"; 167 | this.thing.actionNotify(this); 168 | this.performAction(); 169 | this.finish(); 170 | } 171 | 172 | /** 173 | * Override this with the code necessary to perform the action. 174 | */ 175 | public void performAction() { 176 | } 177 | 178 | /** 179 | * Override this with the code necessary to cancel the action. 180 | */ 181 | public void cancel() { 182 | } 183 | 184 | /** 185 | * Finish performing the action. 186 | */ 187 | public void finish() { 188 | this.status = "completed"; 189 | this.timeCompleted = Utils.timestamp(); 190 | this.thing.actionNotify(this); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/io/webthings/webthing/Event.java: -------------------------------------------------------------------------------- 1 | /** 2 | * High-level Event base class implementation. 3 | */ 4 | package io.webthings.webthing; 5 | 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | /** 10 | * An Event represents an individual event from a thing. 11 | * 12 | * @param The type of the event data. 13 | */ 14 | public class Event { 15 | private final Thing thing; 16 | private final String name; 17 | private final T data; 18 | private final String time; 19 | 20 | /** 21 | * Initialize the object. 22 | * 23 | * @param thing Thing this event belongs to 24 | * @param name Name of the event 25 | */ 26 | public Event(Thing thing, String name) { 27 | this(thing, name, null); 28 | } 29 | 30 | /** 31 | * Initialize the object. 32 | * 33 | * @param thing Thing this event belongs to 34 | * @param name Name of the event 35 | * @param data Data associated with the event 36 | */ 37 | public Event(Thing thing, String name, T data) { 38 | this.thing = thing; 39 | this.name = name; 40 | this.data = data; 41 | this.time = Utils.timestamp(); 42 | } 43 | 44 | /** 45 | * Get the event description. 46 | * 47 | * @return Description of the event as a JSONObject. 48 | */ 49 | public JSONObject asEventDescription() { 50 | JSONObject obj = new JSONObject(); 51 | JSONObject inner = new JSONObject(); 52 | try { 53 | inner.put("timestamp", this.time); 54 | 55 | if (this.data != null) { 56 | inner.put("data", this.data); 57 | } 58 | 59 | obj.put(this.name, inner); 60 | return obj; 61 | } catch (JSONException e) { 62 | return null; 63 | } 64 | } 65 | 66 | /** 67 | * Get the thing associated with this event. 68 | * 69 | * @return The thing. 70 | */ 71 | public Thing getThing() { 72 | return this.thing; 73 | } 74 | 75 | /** 76 | * Get the event's name. 77 | * 78 | * @return The name. 79 | */ 80 | public String getName() { 81 | return this.name; 82 | } 83 | 84 | /** 85 | * Get the event's data. 86 | * 87 | * @return The data. 88 | */ 89 | public T getData() { 90 | return this.data; 91 | } 92 | 93 | /** 94 | * Get the event's timestamp. 95 | * 96 | * @return The time. 97 | */ 98 | public String getTime() { 99 | return this.time; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/io/webthings/webthing/Property.java: -------------------------------------------------------------------------------- 1 | /** 2 | * High-level Property base class implementation. 3 | */ 4 | package io.webthings.webthing; 5 | 6 | import org.everit.json.schema.Schema; 7 | import org.everit.json.schema.ValidationException; 8 | import org.everit.json.schema.loader.SchemaLoader; 9 | import org.json.JSONArray; 10 | import org.json.JSONObject; 11 | 12 | import io.webthings.webthing.errors.PropertyError; 13 | 14 | /** 15 | * A Property represents an individual state value of a thing. 16 | * 17 | * @param The type of the property value. 18 | */ 19 | public class Property { 20 | private final Thing thing; 21 | private final String name; 22 | private String hrefPrefix; 23 | private final String href; 24 | private final JSONObject metadata; 25 | private final Value value; 26 | 27 | /** 28 | * Initialize the object. 29 | * 30 | * @param thing Thing this property belongs to 31 | * @param name Name of the property 32 | * @param value Value object to hold the property value 33 | */ 34 | public Property(Thing thing, String name, Value value) { 35 | this(thing, name, value, null); 36 | } 37 | 38 | /** 39 | * Initialize the object. 40 | * 41 | * @param thing Thing this property belongs to 42 | * @param name Name of the property 43 | * @param value Value object to hold the property value 44 | * @param metadata Property metadata, i.e. type, description, unit, etc., as 45 | * a Map 46 | */ 47 | public Property(Thing thing, 48 | String name, 49 | Value value, 50 | JSONObject metadata) { 51 | this.thing = thing; 52 | this.name = name; 53 | this.value = value; 54 | this.hrefPrefix = ""; 55 | this.href = String.format("/properties/%s", this.name); 56 | 57 | if (metadata == null) { 58 | this.metadata = new JSONObject(); 59 | } else { 60 | this.metadata = metadata; 61 | } 62 | 63 | // Add the property change observer to notify the Thing about a 64 | // property change 65 | this.value.addObserver((a, b) -> this.thing.propertyNotify(this)); 66 | } 67 | 68 | /** 69 | * Validate new property value before setting it. 70 | * 71 | * @param value New value 72 | * @throws PropertyError On validation error. 73 | */ 74 | private void validateValue(T value) throws PropertyError { 75 | if (this.metadata.has("readOnly") && 76 | this.metadata.getBoolean("readOnly")) { 77 | throw new PropertyError("Read-only property"); 78 | } 79 | 80 | Schema schema = SchemaLoader.load(this.metadata); 81 | try { 82 | schema.validate(value); 83 | } catch (ValidationException e) { 84 | throw new PropertyError("Invalid property value"); 85 | } 86 | } 87 | 88 | /** 89 | * Get the property description. 90 | * 91 | * @return Description of the property as an object. 92 | */ 93 | public JSONObject asPropertyDescription() { 94 | JSONObject description = new JSONObject(this.metadata.toString()); 95 | JSONObject link = new JSONObject(); 96 | link.put("rel", "property"); 97 | link.put("href", this.hrefPrefix + this.href); 98 | 99 | if (description.has("links")) { 100 | description.getJSONArray("links").put(link); 101 | } else { 102 | JSONArray links = new JSONArray(); 103 | links.put(link); 104 | description.put("links", links); 105 | } 106 | 107 | return description; 108 | } 109 | 110 | /** 111 | * Set the prefix of any hrefs associated with this property. 112 | * 113 | * @param prefix The prefix 114 | */ 115 | public void setHrefPrefix(String prefix) { 116 | this.hrefPrefix = prefix; 117 | } 118 | 119 | /** 120 | * Get the href of this property. 121 | * 122 | * @return The href. 123 | */ 124 | public String getHref() { 125 | return this.hrefPrefix + this.href; 126 | } 127 | 128 | /** 129 | * Get the current property value. 130 | * 131 | * @return The current value. 132 | */ 133 | public T getValue() { 134 | return this.value.get(); 135 | } 136 | 137 | /** 138 | * Set the current value of the property. 139 | * 140 | * @param value The value to set 141 | * @throws PropertyError If value could not be set. 142 | */ 143 | public void setValue(T value) throws PropertyError { 144 | this.validateValue(value); 145 | this.value.set(value); 146 | } 147 | 148 | /** 149 | * Get the name of this property. 150 | * 151 | * @return The proeprty name. 152 | */ 153 | public String getName() { 154 | return this.name; 155 | } 156 | 157 | /** 158 | * Get the thing associated with this property. 159 | * 160 | * @return The thing. 161 | */ 162 | public Thing getThing() { 163 | return this.thing; 164 | } 165 | 166 | /** 167 | * Get the metadata associated with this property. 168 | * 169 | * @return The metadata. 170 | */ 171 | public JSONObject getMetadata() { 172 | return this.metadata; 173 | } 174 | 175 | /** 176 | * Get the base type of this properties value. 177 | * 178 | * @return The base type. 179 | */ 180 | public Class getBaseType() 181 | { 182 | return value.getBaseType(); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/io/webthings/webthing/Thing.java: -------------------------------------------------------------------------------- 1 | /** 2 | * High-level Thing base class implementation. 3 | */ 4 | package io.webthings.webthing; 5 | 6 | import org.everit.json.schema.Schema; 7 | import org.everit.json.schema.ValidationException; 8 | import org.everit.json.schema.loader.SchemaLoader; 9 | import org.json.JSONArray; 10 | import org.json.JSONException; 11 | import org.json.JSONObject; 12 | 13 | import java.lang.reflect.Constructor; 14 | import java.lang.reflect.InvocationTargetException; 15 | import java.util.ArrayList; 16 | import java.util.HashMap; 17 | import java.util.HashSet; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.Optional; 21 | import java.util.Set; 22 | 23 | import io.webthings.webthing.errors.PropertyError; 24 | 25 | /** 26 | * A Web Thing. 27 | */ 28 | public class Thing { 29 | private final String id; 30 | private final String context; 31 | private final JSONArray type; 32 | private final String title; 33 | private final String description; 34 | private final Map properties; 35 | private final Map availableActions; 36 | private final Map availableEvents; 37 | private final Map> actions; 38 | private final List events; 39 | private final Set subscribers; 40 | private String hrefPrefix; 41 | private String uiHref; 42 | 43 | /** 44 | * Initialize the object. 45 | * 46 | * @param id The thing's unique ID - must be a URI 47 | * @param title The thing's title 48 | */ 49 | public Thing(String id, String title) { 50 | this(id, title, new JSONArray(), ""); 51 | } 52 | 53 | /** 54 | * Initialize the object. 55 | * 56 | * @param id The thing's unique ID - must be a URI 57 | * @param title The thing's title 58 | * @param type The thing's type(s) 59 | */ 60 | public Thing(String id, String title, JSONArray type) { 61 | this(id, title, type, ""); 62 | } 63 | 64 | /** 65 | * Initialize the object. 66 | * 67 | * @param id The thing's unique ID - must be a URI 68 | * @param title The thing's title 69 | * @param type The thing's type(s) 70 | * @param description Description of the thing 71 | */ 72 | public Thing(String id, String title, JSONArray type, String description) { 73 | this.id = id; 74 | this.title = title; 75 | this.context = "https://webthings.io/schemas"; 76 | this.type = type; 77 | this.description = description; 78 | this.properties = new HashMap<>(); 79 | this.availableActions = new HashMap<>(); 80 | this.availableEvents = new HashMap<>(); 81 | this.actions = new HashMap<>(); 82 | this.events = new ArrayList<>(); 83 | this.subscribers = new HashSet<>(); 84 | this.hrefPrefix = ""; 85 | this.uiHref = null; 86 | } 87 | 88 | /** 89 | * Return the thing state as a Thing Description. 90 | * 91 | * @return Current thing state. 92 | */ 93 | public JSONObject asThingDescription() { 94 | JSONObject obj = new JSONObject(); 95 | JSONObject actions = new JSONObject(); 96 | JSONObject events = new JSONObject(); 97 | 98 | this.availableActions.forEach((name, value) -> { 99 | JSONObject metadata = value.getMetadata(); 100 | JSONArray links = new JSONArray(); 101 | JSONObject link = new JSONObject(); 102 | link.put("rel", "action"); 103 | link.put("href", 104 | String.format("%s/actions/%s", this.hrefPrefix, name)); 105 | links.put(link); 106 | metadata.put("links", links); 107 | actions.put(name, metadata); 108 | }); 109 | 110 | this.availableEvents.forEach((name, value) -> { 111 | JSONObject metadata = value.getMetadata(); 112 | JSONArray links = new JSONArray(); 113 | JSONObject link = new JSONObject(); 114 | link.put("rel", "event"); 115 | link.put("href", 116 | String.format("%s/events/%s", this.hrefPrefix, name)); 117 | links.put(link); 118 | metadata.put("links", links); 119 | events.put(name, metadata); 120 | }); 121 | 122 | try { 123 | obj.put("id", this.getId()); 124 | obj.put("title", this.getTitle()); 125 | obj.put("@context", this.getContext()); 126 | obj.put("@type", this.getType()); 127 | obj.put("properties", this.getPropertyDescriptions()); 128 | obj.put("actions", actions); 129 | obj.put("events", events); 130 | 131 | if (this.description != null) { 132 | obj.put("description", this.getDescription()); 133 | } 134 | 135 | JSONObject propertiesLink = new JSONObject(); 136 | propertiesLink.put("rel", "properties"); 137 | propertiesLink.put("href", 138 | String.format("%s/properties", this.hrefPrefix)); 139 | obj.accumulate("links", propertiesLink); 140 | 141 | JSONObject actionsLink = new JSONObject(); 142 | actionsLink.put("rel", "actions"); 143 | actionsLink.put("href", 144 | String.format("%s/actions", this.hrefPrefix)); 145 | obj.accumulate("links", actionsLink); 146 | 147 | JSONObject eventsLink = new JSONObject(); 148 | eventsLink.put("rel", "events"); 149 | eventsLink.put("href", String.format("%s/events", this.hrefPrefix)); 150 | obj.accumulate("links", eventsLink); 151 | 152 | if (this.uiHref != null) { 153 | JSONObject uiLink = new JSONObject(); 154 | uiLink.put("rel", "alternate"); 155 | uiLink.put("mediaType", "text/html"); 156 | uiLink.put("href", this.uiHref); 157 | obj.accumulate("links", uiLink); 158 | } 159 | 160 | return obj; 161 | } catch (JSONException e) { 162 | return null; 163 | } 164 | } 165 | 166 | /** 167 | * Get this thing's href. 168 | * 169 | * @return The href 170 | */ 171 | public String getHref() { 172 | if (this.hrefPrefix.length() > 0) { 173 | return this.hrefPrefix; 174 | } 175 | 176 | return "/"; 177 | } 178 | 179 | /** 180 | * Get this thing's UI href. 181 | * 182 | * @return The href. 183 | */ 184 | public String getUiHref() { 185 | return uiHref; 186 | } 187 | 188 | /** 189 | * Set the href of this thing's custom UI. 190 | * 191 | * @param href The href 192 | */ 193 | public void setUiHref(String href) { 194 | this.uiHref = href; 195 | } 196 | 197 | /** 198 | * Set the prefix of any hrefs associated with this thing. 199 | * 200 | * @param prefix The prefix 201 | */ 202 | public void setHrefPrefix(String prefix) { 203 | this.hrefPrefix = prefix; 204 | 205 | this.properties.forEach((name, value) -> value.setHrefPrefix(prefix)); 206 | 207 | this.actions.forEach((actionName, list) -> list.forEach((action) -> action 208 | .setHrefPrefix(prefix))); 209 | } 210 | 211 | /** 212 | * Get the ID of the thing. 213 | * 214 | * @return The ID. 215 | */ 216 | public String getId() { 217 | return this.id; 218 | } 219 | 220 | /** 221 | * Get the title of the thing. 222 | * 223 | * @return The title. 224 | */ 225 | public String getTitle() { 226 | return this.title; 227 | } 228 | 229 | /** 230 | * Get the type context of the thing. 231 | * 232 | * @return The context. 233 | */ 234 | public String getContext() { 235 | return this.context; 236 | } 237 | 238 | /** 239 | * Get the type(s) of the thing. 240 | * 241 | * @return The type(s). 242 | */ 243 | public JSONArray getType() { 244 | return this.type; 245 | } 246 | 247 | /** 248 | * Get the description of the thing. 249 | * 250 | * @return The description. 251 | */ 252 | public String getDescription() { 253 | return this.description; 254 | } 255 | 256 | /** 257 | * Get the thing's properties as a JSONObject. 258 | * 259 | * @return Properties, i.e. name: description. 260 | */ 261 | public JSONObject getPropertyDescriptions() { 262 | JSONObject obj = new JSONObject(); 263 | 264 | this.properties.forEach((name, value) -> { 265 | try { 266 | obj.put(name, value.asPropertyDescription()); 267 | } catch (JSONException e) { 268 | // pass 269 | } 270 | }); 271 | 272 | return obj; 273 | } 274 | 275 | /** 276 | * Get the thing's actions as a JSONArray. 277 | * 278 | * @param actionName Optional action name to get descriptions for 279 | * @return Action descriptions. 280 | */ 281 | public JSONArray getActionDescriptions(String actionName) { 282 | JSONArray array = new JSONArray(); 283 | 284 | if (actionName == null) { 285 | this.actions.forEach((name, list) -> list.forEach((action) -> array.put( 286 | action.asActionDescription()))); 287 | } else if (this.actions.containsKey(actionName)) { 288 | this.actions.get(actionName) 289 | .forEach((action) -> array.put(action.asActionDescription())); 290 | } 291 | 292 | return array; 293 | } 294 | 295 | /** 296 | * Get the thing's events as a JSONArray. 297 | * 298 | * @param eventName Optional event name to get descriptions for 299 | * @return Event descriptions. 300 | */ 301 | public JSONArray getEventDescriptions(String eventName) { 302 | JSONArray array = new JSONArray(); 303 | 304 | if (eventName == null) { 305 | this.events.forEach((event) -> array.put(event.asEventDescription())); 306 | } else { 307 | this.events.forEach((event) -> { 308 | if (event.getName().equals(eventName)) { 309 | array.put(event.asEventDescription()); 310 | } 311 | }); 312 | } 313 | 314 | return array; 315 | } 316 | 317 | /** 318 | * Add a property to this thing. 319 | * 320 | * @param property Property to add. 321 | */ 322 | public void addProperty(Property property) { 323 | property.setHrefPrefix(this.hrefPrefix); 324 | this.properties.put(property.getName(), property); 325 | } 326 | 327 | /** 328 | * Remove a property from this thing. 329 | * 330 | * @param property Property to remove. 331 | */ 332 | public void removeProperty(Property property) { 333 | this.properties.remove(property.getName()); 334 | } 335 | 336 | /** 337 | * Find a property by name. 338 | * 339 | * @param propertyName Name of the property to find 340 | * @return Property if found, else null. 341 | */ 342 | public Property findProperty(String propertyName) { 343 | if (this.properties.containsKey(propertyName)) { 344 | return this.properties.get(propertyName); 345 | } 346 | 347 | return null; 348 | } 349 | 350 | /** 351 | * Get a property's value. 352 | * 353 | * @param propertyName Name of the property to get the value of 354 | * @param Type of the property value 355 | * @return Current property value if found, else null. 356 | */ 357 | public T getProperty(String propertyName) { 358 | Property prop = this.findProperty(propertyName); 359 | if (prop != null) { 360 | return prop.getValue(); 361 | } 362 | 363 | return null; 364 | } 365 | 366 | /** 367 | * Get a mapping of all properties and their values. 368 | * 369 | * @return JSON object of propertyName -> value. 370 | */ 371 | public JSONObject getProperties() { 372 | JSONObject properties = new JSONObject(); 373 | this.properties.forEach((name, property) -> properties.put(name, 374 | property.getValue())); 375 | return properties; 376 | } 377 | 378 | /** 379 | * Determine whether or not this thing has a given property. 380 | * 381 | * @param propertyName The property to look for 382 | * @return Indication of property presence. 383 | */ 384 | public boolean hasProperty(String propertyName) { 385 | return this.properties.containsKey(propertyName); 386 | } 387 | 388 | /** 389 | * Set a property value. 390 | * 391 | * @param propertyName Name of the property to set 392 | * @param value Value to set 393 | * @param Type of the property value 394 | * @throws PropertyError If value could not be set. 395 | */ 396 | public void setProperty(String propertyName, T value) 397 | throws PropertyError { 398 | Property prop = this.findProperty(propertyName); 399 | if (prop == null) { 400 | return; 401 | } 402 | 403 | Optional converted = Utils.checkIfBaseTypeConversionIsRequired(prop.getBaseType(), value); 404 | if(converted.isPresent()) { 405 | setProperty(propertyName, converted.get()); 406 | return; 407 | } 408 | 409 | prop.setValue(value); 410 | } 411 | 412 | /** 413 | * Get an action. 414 | * 415 | * @param actionName Name of the action 416 | * @param actionId ID of the action 417 | * @return The requested action if found, else null. 418 | */ 419 | public Action getAction(String actionName, String actionId) { 420 | if (!this.actions.containsKey(actionName)) { 421 | return null; 422 | } 423 | 424 | List actions = this.actions.get(actionName); 425 | for (Action action : actions) { 426 | if (actionId.equals(action.getId())) { 427 | return action; 428 | } 429 | } 430 | 431 | return null; 432 | } 433 | 434 | /** 435 | * Add a new event and notify subscribers. 436 | * 437 | * @param event The event that occurred. 438 | */ 439 | public void addEvent(Event event) { 440 | this.events.add(event); 441 | this.eventNotify(event); 442 | } 443 | 444 | /** 445 | * Add an available event. 446 | * 447 | * @param name Name of the event 448 | * @param metadata Event metadata, i.e. type, description, etc., as a 449 | * JSONObject 450 | */ 451 | public void addAvailableEvent(String name, JSONObject metadata) { 452 | if (metadata == null) { 453 | metadata = new JSONObject(); 454 | } 455 | 456 | this.availableEvents.put(name, new AvailableEvent(metadata)); 457 | } 458 | 459 | /** 460 | * Perform an action on the thing. 461 | * 462 | * @param actionName Name of the action 463 | * @param input Any action inputs 464 | * @return The action that was created. 465 | */ 466 | public Action performAction(String actionName, JSONObject input) { 467 | if (!this.availableActions.containsKey(actionName)) { 468 | return null; 469 | } 470 | 471 | AvailableAction actionType = this.availableActions.get(actionName); 472 | if (!actionType.validateActionInput(input)) { 473 | return null; 474 | } 475 | 476 | Class cls = actionType.getCls(); 477 | try { 478 | Constructor constructor = 479 | cls.getConstructor(Thing.class, JSONObject.class); 480 | Action action = 481 | (Action)constructor.newInstance(new Object[]{this, input}); 482 | action.setHrefPrefix(this.hrefPrefix); 483 | this.actionNotify(action); 484 | this.actions.get(actionName).add(action); 485 | return action; 486 | } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { 487 | System.out.println(e); 488 | return null; 489 | } 490 | } 491 | 492 | /** 493 | * Remove an existing action. 494 | * 495 | * @param actionName name of the action 496 | * @param actionId ID of the action 497 | * @return Boolean indicating the presence of the action. 498 | */ 499 | public boolean removeAction(String actionName, String actionId) { 500 | Action action = this.getAction(actionName, actionId); 501 | if (action == null) { 502 | return false; 503 | } 504 | 505 | action.cancel(); 506 | this.actions.get(actionName).remove(action); 507 | return true; 508 | } 509 | 510 | /** 511 | * Add an available action. 512 | * 513 | * @param name Name of the action 514 | * @param metadata Action metadata, i.e. type, description, etc., as a 515 | * JSONObject 516 | * @param cls Class to instantiate for this action 517 | */ 518 | public void addAvailableAction(String name, 519 | JSONObject metadata, 520 | Class cls) { 521 | if (metadata == null) { 522 | metadata = new JSONObject(); 523 | } 524 | 525 | this.availableActions.put(name, new AvailableAction(metadata, cls)); 526 | this.actions.put(name, new ArrayList<>()); 527 | } 528 | 529 | /** 530 | * Add a new websocket subscriber. 531 | * 532 | * @param ws The websocket 533 | */ 534 | public void addSubscriber(WebThingServer.ThingHandler.ThingWebSocket ws) { 535 | this.subscribers.add(ws); 536 | } 537 | 538 | /** 539 | * Remove a websocket subscriber. 540 | * 541 | * @param ws The websocket 542 | */ 543 | public void removeSubscriber(WebThingServer.ThingHandler.ThingWebSocket ws) { 544 | this.subscribers.remove(ws); 545 | 546 | this.availableEvents.forEach((name, value) -> this.removeEventSubscriber( 547 | name, 548 | ws)); 549 | } 550 | 551 | /** 552 | * Add a new websocket subscriber to an event. 553 | * 554 | * @param name Name of the event 555 | * @param ws The websocket 556 | */ 557 | public void addEventSubscriber(String name, 558 | WebThingServer.ThingHandler.ThingWebSocket ws) { 559 | if (this.availableEvents.containsKey(name)) { 560 | this.availableEvents.get(name).addSubscriber(ws); 561 | } 562 | } 563 | 564 | /** 565 | * Remove a websocket subscriber from an event. 566 | * 567 | * @param name Name of the event 568 | * @param ws The websocket 569 | */ 570 | public void removeEventSubscriber(String name, 571 | WebThingServer.ThingHandler.ThingWebSocket ws) { 572 | if (this.availableEvents.containsKey(name)) { 573 | this.availableEvents.get(name).removeSubscriber(ws); 574 | } 575 | } 576 | 577 | /** 578 | * Notify all subscribers of a property change. 579 | * 580 | * @param property The property that changed 581 | */ 582 | public void propertyNotify(Property property) { 583 | JSONObject json = new JSONObject(); 584 | JSONObject inner = new JSONObject(); 585 | 586 | inner.put(property.getName(), property.getValue()); 587 | json.put("messageType", "propertyStatus"); 588 | json.put("data", inner); 589 | 590 | String message = json.toString(); 591 | 592 | this.subscribers.forEach((subscriber) -> subscriber.sendMessage(message)); 593 | } 594 | 595 | /** 596 | * Notify all subscribers of an action status change. 597 | * 598 | * @param action The action whose status changed 599 | */ 600 | public void actionNotify(Action action) { 601 | JSONObject json = new JSONObject(); 602 | 603 | json.put("messageType", "actionStatus"); 604 | json.put("data", action.asActionDescription()); 605 | 606 | String message = json.toString(); 607 | 608 | this.subscribers.forEach((subscriber) -> subscriber.sendMessage(message)); 609 | } 610 | 611 | /** 612 | * Notify all subscribers of an event. 613 | * 614 | * @param event The event that occurred 615 | */ 616 | public void eventNotify(Event event) { 617 | String eventName = event.getName(); 618 | if (!this.availableEvents.containsKey(eventName)) { 619 | return; 620 | } 621 | 622 | JSONObject json = new JSONObject(); 623 | 624 | json.put("messageType", "event"); 625 | json.put("data", event.asEventDescription()); 626 | 627 | String message = json.toString(); 628 | 629 | this.availableEvents.get(eventName) 630 | .getSubscribers() 631 | .forEach((subscriber) -> subscriber.sendMessage( 632 | message)); 633 | } 634 | 635 | /** 636 | * Class to describe an event available for subscription. 637 | */ 638 | private static class AvailableEvent { 639 | private final JSONObject metadata; 640 | private final Set 641 | subscribers; 642 | 643 | /** 644 | * Initialize the object. 645 | * 646 | * @param metadata The event metadata 647 | */ 648 | public AvailableEvent(JSONObject metadata) { 649 | this.metadata = metadata; 650 | this.subscribers = new HashSet<>(); 651 | } 652 | 653 | /** 654 | * Get the event metadata. 655 | * 656 | * @return The metadata. 657 | */ 658 | public JSONObject getMetadata() { 659 | return this.metadata; 660 | } 661 | 662 | /** 663 | * Add a websocket subscriber to the event. 664 | * 665 | * @param ws The websocket 666 | */ 667 | public void addSubscriber(WebThingServer.ThingHandler.ThingWebSocket ws) { 668 | this.subscribers.add(ws); 669 | } 670 | 671 | /** 672 | * Remove a websocket subscriber from the event. 673 | * 674 | * @param ws The websocket 675 | */ 676 | public void removeSubscriber(WebThingServer.ThingHandler.ThingWebSocket ws) { 677 | this.subscribers.remove(ws); 678 | } 679 | 680 | /** 681 | * Get the set of subscribers for the event. 682 | * 683 | * @return The set of subscribers. 684 | */ 685 | public Set getSubscribers() { 686 | return this.subscribers; 687 | } 688 | } 689 | 690 | /** 691 | * Class to describe an action available to be taken. 692 | */ 693 | private static class AvailableAction { 694 | private final JSONObject metadata; 695 | private final Class cls; 696 | private final Schema schema; 697 | 698 | /** 699 | * Initialize the object. 700 | * 701 | * @param metadata The action metadata 702 | * @param cls Class to instantiate for the action 703 | */ 704 | public AvailableAction(JSONObject metadata, Class cls) { 705 | this.metadata = metadata; 706 | this.cls = cls; 707 | 708 | if (metadata.has("input")) { 709 | JSONObject rawSchema = metadata.getJSONObject("input"); 710 | this.schema = SchemaLoader.load(rawSchema); 711 | } else { 712 | this.schema = null; 713 | } 714 | } 715 | 716 | /** 717 | * Get the action metadata. 718 | * 719 | * @return The metadata. 720 | */ 721 | public JSONObject getMetadata() { 722 | return this.metadata; 723 | } 724 | 725 | /** 726 | * Get the class to instantiate for the action. 727 | * 728 | * @return The class. 729 | */ 730 | public Class getCls() { 731 | return this.cls; 732 | } 733 | 734 | /** 735 | * Validate the input for a new action. 736 | * 737 | * @param actionInput The input to validate 738 | * @return Boolean indicating validation success. 739 | */ 740 | public boolean validateActionInput(JSONObject actionInput) { 741 | if (this.schema == null) { 742 | return true; 743 | } 744 | 745 | if (actionInput == null) { 746 | actionInput = new JSONObject(); 747 | } 748 | 749 | try { 750 | this.schema.validate(actionInput); 751 | } catch (ValidationException e) { 752 | return false; 753 | } 754 | 755 | return true; 756 | } 757 | } 758 | } 759 | -------------------------------------------------------------------------------- /src/main/java/io/webthings/webthing/Utils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility functions. 3 | */ 4 | package io.webthings.webthing; 5 | 6 | import java.net.InetAddress; 7 | import java.net.NetworkInterface; 8 | import java.net.SocketException; 9 | import java.time.Instant; 10 | import java.util.ArrayList; 11 | import java.util.Arrays; 12 | import java.util.Collections; 13 | import java.util.HashMap; 14 | import java.util.HashSet; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.Optional; 18 | import java.util.Set; 19 | import java.util.function.Function; 20 | 21 | public class Utils { 22 | /** 23 | * Get the current time. 24 | * 25 | * @return The current time in the form YYYY-mm-ddTHH:MM:SS+00.00 26 | */ 27 | public static String timestamp() { 28 | String now = Instant.now().toString().split("\\.")[0]; 29 | return now + "+00:00"; 30 | } 31 | 32 | /** 33 | * Get all IP addresses. 34 | * 35 | * @return List of addresses. 36 | */ 37 | public static List getAddresses() { 38 | Set addresses = new HashSet<>(); 39 | try { 40 | for (NetworkInterface iface : Collections.list(NetworkInterface.getNetworkInterfaces())) { 41 | for (InetAddress address : Collections.list(iface.getInetAddresses())) { 42 | // Sometimes, IPv6 addresses will have the interface name 43 | // appended as, e.g. %eth0. Handle that. 44 | String s = address.getHostAddress() 45 | .split("%")[0].toLowerCase(); 46 | 47 | // Filter out link-local addresses. 48 | if (s.contains(":")) { 49 | if (!s.startsWith("fe80:")) { 50 | s = s.replaceFirst("(^|:)(0+(:|$)){2,8}", "::"); 51 | addresses.add(String.format("[%s]", s)); 52 | } 53 | } else { 54 | if (!s.startsWith("169.254.")) { 55 | addresses.add(s); 56 | } 57 | } 58 | } 59 | } 60 | } catch (SocketException e) { 61 | return Arrays.asList("127.0.0.1"); 62 | } 63 | 64 | List ret = new ArrayList<>(addresses); 65 | Collections.sort(ret); 66 | return ret; 67 | } 68 | 69 | static Map, Function> createNumberConverters() 70 | { 71 | Map, Function> converters = new HashMap<>(); 72 | converters.put(Integer.class, n -> n.intValue()); 73 | converters.put(Long.class, n -> n.longValue()); 74 | converters.put(Float.class, n -> n.floatValue()); 75 | converters.put(Double.class, n -> n.doubleValue()); 76 | return converters; 77 | } 78 | final static Map, Function> supportedNumberConverters = createNumberConverters(); 79 | 80 | /** 81 | * Performes a type conversion when required. 82 | * @param baseTypeClass The Class of the base type to check agains 83 | * @param value Value that might need conversion 84 | * @return Optional containing converted value or empty Optional when no conversion was required. 85 | */ 86 | public static Optional checkIfBaseTypeConversionIsRequired(Class baseTypeClass, T value) { 87 | if(value instanceof Number && baseTypeClass != value.getClass()) { 88 | if(Number.class.isAssignableFrom(baseTypeClass)) { 89 | return Optional.ofNullable(supportedNumberConverters.get(baseTypeClass)).map(f -> f.apply((Number) value)); 90 | } 91 | } 92 | return Optional.empty(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/io/webthings/webthing/Value.java: -------------------------------------------------------------------------------- 1 | package io.webthings.webthing; 2 | 3 | import java.util.Objects; 4 | import java.util.Observable; 5 | import java.util.function.Consumer; 6 | 7 | /** 8 | * A property value. 9 | *

10 | * This is used for communicating between the Thing representation and the 11 | * actual physical thing implementation. 12 | *

13 | * Notifies all observers when the underlying value changes through an external 14 | * update (command to turn the light off) or if the underlying sensor reports a 15 | * new value. 16 | * 17 | * @author Tim Hinkes (timmeey@timmeey.de) 18 | */ 19 | public class Value extends Observable { 20 | 21 | static class BaseTypeHelper 22 | { 23 | static Class derive(final BT initialValue) { 24 | Objects.requireNonNull(initialValue, "Can not derive base type from null."); 25 | return (Class) initialValue.getClass(); 26 | } 27 | } 28 | 29 | private final Consumer valueForwarder; 30 | private final Class baseType; 31 | private T lastValue; 32 | 33 | /** 34 | * Create a read only value that can only be updated by a Thing's reading. 35 | *

36 | * Example: A sensor is updating its reading, but the reading cannot be set 37 | * externally. 38 | * 39 | * @param initialValue The initial value 40 | */ 41 | public Value(final T initialValue) { 42 | this(BaseTypeHelper.derive(initialValue), initialValue, null); 43 | } 44 | 45 | /** 46 | * Create a read only value that can only be updated by a Thing's reading. 47 | * Initial value will be set to null. 48 | *

49 | * Example: A sensor is updating its reading, but the reading cannot be set 50 | * externally. 51 | * 52 | * @param baseType The Class of the values base type 53 | */ 54 | public Value(final Class baseType) { 55 | this(baseType, null, null); 56 | } 57 | 58 | /** 59 | * Create a writable value that can be set to a new value. 60 | *

61 | * Example: A light that can be switched off by setting this to false. 62 | * 63 | * @param initialValue The initial value 64 | * @param valueForwarder The method that updates the actual value on the 65 | * thing 66 | */ 67 | public Value(final T initialValue, final Consumer valueForwarder) { 68 | this(BaseTypeHelper.derive(initialValue), initialValue, valueForwarder); 69 | } 70 | 71 | /** 72 | * Create a writable value that can be set to a new value. 73 | * Initial value will be set to null. 74 | *

75 | * Example: A light that can be switched off by setting this to false. 76 | * 77 | * @param baseType The Class of the values base type 78 | * @param valueForwarder The method that updates the actual value on the 79 | * thing 80 | */ 81 | public Value(final Class baseType, final Consumer valueForwarder) { 82 | this(baseType, null, valueForwarder); 83 | } 84 | 85 | /** 86 | * Create a writable value that can be set to a new value. 87 | *

88 | * Example: A light that can be switched off by setting this to false. 89 | * 90 | * @param baseType The Class of the values base type 91 | * @param initialValue The initial value 92 | * @param valueForwarder The method that updates the actual value on the 93 | * thing 94 | */ 95 | public Value(final Class baseType, final T initialValue, final Consumer valueForwarder) { 96 | Objects.requireNonNull(baseType, "The base type of a value must not be null."); 97 | this.baseType = baseType; 98 | this.lastValue = initialValue; 99 | this.valueForwarder = valueForwarder; 100 | } 101 | 102 | /** 103 | * Get the base type of this value. 104 | * 105 | * @return The base type. 106 | */ 107 | public Class getBaseType() 108 | { 109 | return this.baseType; 110 | } 111 | 112 | /** 113 | * Set a new Value for this thing. 114 | *

115 | * Example: Switch a light off: set(false) 116 | * 117 | * @param value Value to set 118 | */ 119 | public final void set(T value) { 120 | if (valueForwarder != null) { 121 | valueForwarder.accept(value); 122 | } 123 | 124 | this.notifyOfExternalUpdate(value); 125 | } 126 | 127 | /** 128 | * Returns the last known value from the underlying thing. 129 | *

130 | * Example: Returns false, when a light is off. 131 | * 132 | * @return The value. 133 | */ 134 | public final T get() { 135 | return this.lastValue; 136 | } 137 | 138 | /** 139 | * Called if the underlying thing reported a new value. This informs 140 | * observers about the update. 141 | *

142 | * Example: A sensor reports a new value. 143 | * 144 | * @param value the newly reported value 145 | */ 146 | public final void notifyOfExternalUpdate(T value) { 147 | if (value != null && !value.equals(this.lastValue)) { 148 | this.setChanged(); 149 | this.lastValue = value; 150 | notifyObservers(value); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/io/webthings/webthing/WebThingServer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Java Web Thing server implementation. 3 | */ 4 | package io.webthings.webthing; 5 | 6 | import org.json.JSONArray; 7 | import org.json.JSONException; 8 | import org.json.JSONObject; 9 | 10 | import java.io.IOException; 11 | import java.net.InetAddress; 12 | import java.security.NoSuchAlgorithmException; 13 | import java.util.ArrayList; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.Timer; 18 | import java.util.TimerTask; 19 | 20 | import javax.jmdns.JmDNS; 21 | import javax.jmdns.ServiceInfo; 22 | import javax.net.ssl.SSLServerSocketFactory; 23 | 24 | import fi.iki.elonen.NanoHTTPD; 25 | import fi.iki.elonen.NanoWSD; 26 | import fi.iki.elonen.router.RouterNanoHTTPD; 27 | import io.webthings.webthing.errors.PropertyError; 28 | 29 | /** 30 | * Server to represent a Web Thing over HTTP. 31 | */ 32 | public class WebThingServer extends RouterNanoHTTPD { 33 | private static final int SOCKET_READ_TIMEOUT = 30 * 1000; 34 | private static final int WEBSOCKET_PING_INTERVAL = 20 * 1000; 35 | private final int port; 36 | private final ThingsType things; 37 | private final String name; 38 | private String hostname; 39 | private final boolean disableHostValidation; 40 | private final String basePath; 41 | private final List hosts; 42 | private final boolean isTls; 43 | private JmDNS jmdns; 44 | 45 | /** 46 | * Initialize the WebThingServer on port 80. 47 | * 48 | * @param things List of Things managed by this server 49 | * @throws IOException If server fails to bind. 50 | * @throws NullPointerException If something bad happened. 51 | */ 52 | public WebThingServer(ThingsType things) 53 | throws IOException, NullPointerException { 54 | this(things, 80, null, null, null, "/", false); 55 | } 56 | 57 | /** 58 | * Initialize the WebThingServer. 59 | * 60 | * @param things List of Things managed by this server 61 | * @param port Port to listen on 62 | * @throws IOException If server fails to bind. 63 | * @throws NullPointerException If something bad happened. 64 | */ 65 | public WebThingServer(ThingsType things, int port) 66 | throws IOException, NullPointerException { 67 | this(things, port, null, null, null, "/", false); 68 | } 69 | 70 | /** 71 | * Initialize the WebThingServer. 72 | * 73 | * @param things List of Things managed by this server 74 | * @param port Port to listen on 75 | * @param hostname Host name, i.e. mything.com 76 | * @throws IOException If server fails to bind. 77 | * @throws NullPointerException If something bad happened. 78 | */ 79 | public WebThingServer(ThingsType things, int port, String hostname) 80 | throws IOException, NullPointerException { 81 | this(things, port, hostname, null, null, "/", false); 82 | } 83 | 84 | /** 85 | * Initialize the WebThingServer. 86 | * 87 | * @param things List of Things managed by this server 88 | * @param port Port to listen on 89 | * @param hostname Host name, i.e. mything.com 90 | * @param sslOptions SSL options to pass to the NanoHTTPD server 91 | * @throws IOException If server fails to bind. 92 | * @throws NullPointerException If something bad happened. 93 | */ 94 | public WebThingServer(ThingsType things, 95 | int port, 96 | String hostname, 97 | SSLOptions sslOptions) 98 | throws IOException, NullPointerException { 99 | this(things, port, hostname, sslOptions, null, "/", false); 100 | } 101 | 102 | /** 103 | * Initialize the WebThingServer. 104 | * 105 | * @param things List of Things managed by this server 106 | * @param port Port to listen on 107 | * @param hostname Host name, i.e. mything.com 108 | * @param sslOptions SSL options to pass to the NanoHTTPD server 109 | * @param additionalRoutes List of additional routes to add to the server 110 | * @throws IOException If server fails to bind. 111 | * @throws NullPointerException If something bad happened. 112 | */ 113 | public WebThingServer(ThingsType things, 114 | int port, 115 | String hostname, 116 | SSLOptions sslOptions, 117 | List additionalRoutes) 118 | throws IOException, NullPointerException { 119 | this(things, port, hostname, sslOptions, additionalRoutes, "/", false); 120 | } 121 | 122 | /** 123 | * Initialize the WebThingServer. 124 | * 125 | * @param things List of Things managed by this server 126 | * @param port Port to listen on 127 | * @param hostname Host name, i.e. mything.com 128 | * @param sslOptions SSL options to pass to the NanoHTTPD server 129 | * @param additionalRoutes List of additional routes to add to the server 130 | * @param basePath Base URL path to use, rather than '/' 131 | * @throws IOException If server fails to bind. 132 | * @throws NullPointerException If something bad happened. 133 | */ 134 | public WebThingServer(ThingsType things, 135 | int port, 136 | String hostname, 137 | SSLOptions sslOptions, 138 | List additionalRoutes, 139 | String basePath) 140 | throws IOException, NullPointerException { 141 | this(things, 142 | port, 143 | hostname, 144 | sslOptions, 145 | additionalRoutes, 146 | basePath, 147 | false); 148 | } 149 | 150 | /** 151 | * Initialize the WebThingServer. 152 | * 153 | * @param things List of Things managed by this server 154 | * @param port Port to listen on 155 | * @param hostname Host name, i.e. mything.com 156 | * @param sslOptions SSL options to pass to the NanoHTTPD server 157 | * @param additionalRoutes List of additional routes to add to the 158 | * server 159 | * @param basePath Base URL path to use, rather than '/' 160 | * @param disableHostValidation Whether or not to disable host validation -- 161 | * note that this can lead to DNS rebinding 162 | * attacks 163 | * @throws IOException If server fails to bind. 164 | * @throws NullPointerException If something bad happened. 165 | */ 166 | public WebThingServer(ThingsType things, 167 | int port, 168 | String hostname, 169 | SSLOptions sslOptions, 170 | List additionalRoutes, 171 | String basePath, 172 | boolean disableHostValidation) 173 | throws IOException, NullPointerException { 174 | super(port); 175 | this.port = port; 176 | this.things = things; 177 | this.name = things.getName(); 178 | this.isTls = sslOptions != null; 179 | this.hostname = hostname; 180 | this.basePath = basePath.replaceAll("/$", ""); 181 | this.disableHostValidation = disableHostValidation; 182 | 183 | this.hosts = new ArrayList<>(); 184 | this.hosts.add("localhost"); 185 | this.hosts.add(String.format("localhost:%d", this.port)); 186 | 187 | for (String address : Utils.getAddresses()) { 188 | this.hosts.add(address); 189 | this.hosts.add(String.format("%s:%d", address, this.port)); 190 | } 191 | 192 | if (this.hostname != null) { 193 | this.hostname = this.hostname.toLowerCase(); 194 | this.hosts.add(this.hostname); 195 | this.hosts.add(String.format("%s:%d", this.hostname, this.port)); 196 | } 197 | 198 | if (this.isTls) { 199 | super.makeSecure(sslOptions.getSocketFactory(), 200 | sslOptions.getProtocols()); 201 | } 202 | 203 | this.setRoutePrioritizer(new InsertionOrderRoutePrioritizer()); 204 | 205 | if (additionalRoutes != null && additionalRoutes.size() > 0) { 206 | additionalRoutes.forEach(o -> addRoute(this.basePath + o.url, 207 | o.handlerClass, 208 | o.parameters)); 209 | } 210 | 211 | if (MultipleThings.class.isInstance(things)) { 212 | List list = things.getThings(); 213 | for (int i = 0; i < list.size(); ++i) { 214 | Thing thing = list.get(i); 215 | thing.setHrefPrefix(String.format("%s/%d", this.basePath, i)); 216 | } 217 | 218 | // These are matched in the order they are added. 219 | addRoute(this.basePath + "/:thingId/properties/:propertyName", 220 | PropertyHandler.class, 221 | this.things, 222 | this.hosts, 223 | this.isTls, 224 | this.disableHostValidation); 225 | addRoute(this.basePath + "/:thingId/properties", 226 | PropertiesHandler.class, 227 | this.things, 228 | this.hosts, 229 | this.isTls, 230 | this.disableHostValidation); 231 | addRoute(this.basePath + "/:thingId/actions/:actionName/:actionId", 232 | ActionIDHandler.class, 233 | this.things, 234 | this.hosts, 235 | this.isTls, 236 | this.disableHostValidation); 237 | addRoute(this.basePath + "/:thingId/actions/:actionName", 238 | ActionHandler.class, 239 | this.things, 240 | this.hosts, 241 | this.isTls, 242 | this.disableHostValidation); 243 | addRoute(this.basePath + "/:thingId/actions", 244 | ActionsHandler.class, 245 | this.things, 246 | this.hosts, 247 | this.isTls, 248 | this.disableHostValidation); 249 | addRoute(this.basePath + "/:thingId/events/:eventName", 250 | EventHandler.class, 251 | this.things, 252 | this.hosts, 253 | this.isTls, 254 | this.disableHostValidation); 255 | addRoute(this.basePath + "/:thingId/events", 256 | EventsHandler.class, 257 | this.things, 258 | this.hosts, 259 | this.isTls, 260 | this.disableHostValidation); 261 | addRoute(this.basePath + "/:thingId", 262 | ThingHandler.class, 263 | this.things, 264 | this.hosts, 265 | this.isTls, 266 | this.disableHostValidation); 267 | addRoute(this.basePath + "/", 268 | ThingsHandler.class, 269 | this.things, 270 | this.hosts, 271 | this.isTls, 272 | this.disableHostValidation); 273 | } else { 274 | things.getThing(0).setHrefPrefix(this.basePath); 275 | 276 | // These are matched in the order they are added. 277 | addRoute(this.basePath + "/properties/:propertyName", 278 | PropertyHandler.class, 279 | this.things, 280 | this.hosts, 281 | this.isTls, 282 | this.disableHostValidation); 283 | addRoute(this.basePath + "/properties", 284 | PropertiesHandler.class, 285 | this.things, 286 | this.hosts, 287 | this.isTls, 288 | this.disableHostValidation); 289 | addRoute(this.basePath + "/actions/:actionName/:actionId", 290 | ActionIDHandler.class, 291 | this.things, 292 | this.hosts, 293 | this.isTls, 294 | this.disableHostValidation); 295 | addRoute(this.basePath + "/actions/:actionName", 296 | ActionHandler.class, 297 | this.things, 298 | this.hosts, 299 | this.isTls, 300 | this.disableHostValidation); 301 | addRoute(this.basePath + "/actions", 302 | ActionsHandler.class, 303 | this.things, 304 | this.hosts, 305 | this.isTls, 306 | this.disableHostValidation); 307 | addRoute(this.basePath + "/events/:eventName", 308 | EventHandler.class, 309 | this.things, 310 | this.hosts, 311 | this.isTls, 312 | this.disableHostValidation); 313 | addRoute(this.basePath + "/events", 314 | EventsHandler.class, 315 | this.things, 316 | this.hosts, 317 | this.isTls, 318 | this.disableHostValidation); 319 | addRoute(this.basePath + "/", 320 | ThingHandler.class, 321 | this.things, 322 | this.hosts, 323 | this.isTls, 324 | this.disableHostValidation); 325 | } 326 | 327 | setNotFoundHandler(Error404UriHandler.class); 328 | } 329 | 330 | /** 331 | * Start listening for incoming connections. 332 | * 333 | * @param daemon Whether or not to daemonize the server 334 | * @throws IOException on failure to listen on port 335 | */ 336 | public void start(boolean daemon) throws IOException { 337 | this.jmdns = JmDNS.create(hostname == null ? 338 | InetAddress.getLocalHost() : 339 | InetAddress.getByName(hostname)); 340 | 341 | String systemHostname = this.jmdns.getHostName(); 342 | if (systemHostname.endsWith(".")) { 343 | systemHostname = 344 | systemHostname.substring(0, systemHostname.length() - 1); 345 | } 346 | this.hosts.add(systemHostname); 347 | this.hosts.add(String.format("%s:%d", systemHostname, this.port)); 348 | 349 | Map txt = new HashMap(); 350 | txt.put("path", "/"); 351 | 352 | if (this.isTls) { 353 | txt.put("tls", "1"); 354 | } 355 | 356 | ServiceInfo serviceInfo = ServiceInfo.create("_webthing._tcp.local", 357 | this.name, 358 | null, 359 | this.port, 360 | 0, 361 | 0, 362 | txt); 363 | this.jmdns.registerService(serviceInfo); 364 | 365 | super.start(WebThingServer.SOCKET_READ_TIMEOUT, daemon); 366 | } 367 | 368 | /** 369 | * Stop listening. 370 | */ 371 | public void stop() { 372 | this.jmdns.unregisterAllServices(); 373 | super.stop(); 374 | } 375 | 376 | interface ThingsType { 377 | /** 378 | * Get the thing at the given index. 379 | * 380 | * @param idx Index of thing. 381 | * @return The thing, or null. 382 | */ 383 | Thing getThing(int idx); 384 | 385 | /** 386 | * Get the list of things. 387 | * 388 | * @return The list of things. 389 | */ 390 | List getThings(); 391 | 392 | /** 393 | * Get the mDNS server name. 394 | * 395 | * @return The server name. 396 | */ 397 | String getName(); 398 | } 399 | 400 | /** 401 | * Thread to perform an action. 402 | */ 403 | private static class ActionRunner extends Thread { 404 | private final Action action; 405 | 406 | /** 407 | * Initialize the object. 408 | * 409 | * @param action The action to perform 410 | */ 411 | public ActionRunner(Action action) { 412 | this.action = action; 413 | } 414 | 415 | /** 416 | * Perform the action. 417 | */ 418 | public void run() { 419 | this.action.start(); 420 | } 421 | } 422 | 423 | /** 424 | * Class to hold options required by SSL server. 425 | */ 426 | public static class SSLOptions { 427 | private final String path; 428 | private final String password; 429 | private final String[] protocols; 430 | 431 | /** 432 | * Initialize the object. 433 | * 434 | * @param keystorePath Path to the Java keystore (.jks) file 435 | * @param keystorePassword Password to open the keystore 436 | */ 437 | public SSLOptions(String keystorePath, String keystorePassword) { 438 | this(keystorePath, keystorePassword, null); 439 | } 440 | 441 | /** 442 | * Initialize the object. 443 | * 444 | * @param keystorePath Path to the Java keystore (.jks) file 445 | * @param keystorePassword Password to open the keystore 446 | * @param protocols List of protocols to enable. Documentation 447 | * found here: https://docs.oracle.com/javase/8/docs/api/javax/net/ssl/SSLServerSocket.html#setEnabledProtocols-java.lang.String:A- 448 | */ 449 | public SSLOptions(String keystorePath, 450 | String keystorePassword, 451 | String[] protocols) { 452 | this.path = keystorePath; 453 | this.password = keystorePassword; 454 | this.protocols = protocols; 455 | } 456 | 457 | /** 458 | * Create an SSLServerSocketFactory as required by NanoHTTPD. 459 | * 460 | * @return The socket factory. 461 | * @throws IOException If server fails to bind. 462 | */ 463 | public SSLServerSocketFactory getSocketFactory() throws IOException { 464 | return NanoHTTPD.makeSSLSocketFactory(this.path, 465 | this.password.toCharArray()); 466 | } 467 | 468 | /** 469 | * Get the list of enabled protocols. 470 | * 471 | * @return The list of protocols. 472 | */ 473 | public String[] getProtocols() { 474 | return this.protocols; 475 | } 476 | } 477 | 478 | /** 479 | * Base handler that responds to every request with a 405 Method Not 480 | * Allowed. 481 | */ 482 | public static class BaseHandler implements UriResponder { 483 | /** 484 | * Add necessary CORS headers to response. 485 | * 486 | * @param response Response to add headers to 487 | * @return The Response object. 488 | */ 489 | public Response corsResponse(Response response) { 490 | response.addHeader("Access-Control-Allow-Origin", "*"); 491 | response.addHeader("Access-Control-Allow-Headers", 492 | "Origin, X-Requested-With, Content-Type, Accept"); 493 | response.addHeader("Access-Control-Allow-Methods", 494 | "GET, HEAD, PUT, POST, DELETE"); 495 | return response; 496 | } 497 | 498 | /** 499 | * Handle a GET request. 500 | * 501 | * @param uriResource The URI resource that was matched 502 | * @param urlParams Map of URL parameters 503 | * @param session The HTTP session 504 | * @return 405 Method Not Allowed response. 505 | */ 506 | public Response get(UriResource uriResource, 507 | Map urlParams, 508 | IHTTPSession session) { 509 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED, 510 | null, 511 | null)); 512 | } 513 | 514 | /** 515 | * Handle a PUT request. 516 | * 517 | * @param uriResource The URI resource that was matched 518 | * @param urlParams Map of URL parameters 519 | * @param session The HTTP session 520 | * @return 405 Method Not Allowed response. 521 | */ 522 | public Response put(UriResource uriResource, 523 | Map urlParams, 524 | IHTTPSession session) { 525 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED, 526 | null, 527 | null)); 528 | } 529 | 530 | /** 531 | * Handle a POST request. 532 | * 533 | * @param uriResource The URI resource that was matched 534 | * @param urlParams Map of URL parameters 535 | * @param session The HTTP session 536 | * @return 405 Method Not Allowed response. 537 | */ 538 | public Response post(UriResource uriResource, 539 | Map urlParams, 540 | IHTTPSession session) { 541 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED, 542 | null, 543 | null)); 544 | } 545 | 546 | /** 547 | * Handle a DELETE request. 548 | * 549 | * @param uriResource The URI resource that was matched 550 | * @param urlParams Map of URL parameters 551 | * @param session The HTTP session 552 | * @return 405 Method Not Allowed response. 553 | */ 554 | public Response delete(UriResource uriResource, 555 | Map urlParams, 556 | IHTTPSession session) { 557 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED, 558 | null, 559 | null)); 560 | } 561 | 562 | /** 563 | * Handle any other request. 564 | * 565 | * @param method The HTTP method 566 | * @param uriResource The URI resource that was matched 567 | * @param urlParams Map of URL parameters 568 | * @param session The HTTP session 569 | * @return 405 Method Not Allowed response. 570 | */ 571 | public Response other(String method, 572 | UriResource uriResource, 573 | Map urlParams, 574 | IHTTPSession session) { 575 | if (method.equals("OPTIONS")) { 576 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NO_CONTENT, 577 | null, 578 | null)); 579 | } 580 | 581 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED, 582 | null, 583 | null)); 584 | } 585 | 586 | /** 587 | * Get a parameter from the URI. 588 | * 589 | * @param uri The URI 590 | * @param index Index of the parameter 591 | * @return The URI parameter, or null if index was invalid. 592 | */ 593 | public String getUriParam(String uri, int index) { 594 | String[] parts = uri.split("/"); 595 | if (parts.length <= index) { 596 | return null; 597 | } 598 | 599 | return parts[index]; 600 | } 601 | 602 | /** 603 | * Parse a JSON body. 604 | * 605 | * @param session The HTTP session 606 | * @return The parsed JSON body as a JSONObject, or null on error. 607 | */ 608 | public JSONObject parseBody(IHTTPSession session) { 609 | int contentLength = Integer.parseInt(session.getHeaders() 610 | .get("content-length")); 611 | byte[] buffer = new byte[contentLength]; 612 | try { 613 | session.getInputStream().read(buffer, 0, contentLength); 614 | return new JSONObject(new String(buffer)); 615 | } catch (IOException e) { 616 | return null; 617 | } 618 | } 619 | 620 | /** 621 | * Get the thing this request is for. 622 | * 623 | * @param uriResource The URI resource that was matched 624 | * @param session The HTTP session 625 | * @return The thing, or null if not found. 626 | */ 627 | public Thing getThing(UriResource uriResource, IHTTPSession session) { 628 | ThingsType things = uriResource.initParameter(0, ThingsType.class); 629 | 630 | String thingId = this.getUriParam(session.getUri(), 1); 631 | int id; 632 | try { 633 | id = Integer.parseInt(thingId); 634 | } catch (NumberFormatException e) { 635 | id = 0; 636 | } 637 | 638 | return things.getThing(id); 639 | } 640 | 641 | /** 642 | * Validate Host header. 643 | * 644 | * @param uriResource The URI resource that was matched 645 | * @param session The HTTP session 646 | * @return Boolean indicating validation success. 647 | */ 648 | public boolean validateHost(UriResource uriResource, 649 | IHTTPSession session) { 650 | boolean disableHostValidation = 651 | uriResource.initParameter(3, Boolean.class); 652 | 653 | if (disableHostValidation) { 654 | return true; 655 | } 656 | 657 | List hosts = uriResource.initParameter(1, List.class); 658 | 659 | String host = session.getHeaders().get("host"); 660 | return (host != null && hosts.contains(host.toLowerCase())); 661 | } 662 | 663 | /** 664 | * Determine whether or not this request is HTTPS. 665 | * 666 | * @param uriResource The URI resource that was matched 667 | * @return Boolean indicating whether or not the request is secure. 668 | */ 669 | public boolean isSecure(UriResource uriResource) { 670 | return uriResource.initParameter(2, Boolean.class); 671 | } 672 | } 673 | 674 | /** 675 | * Handle a request to / when the server manages multiple things. 676 | */ 677 | public static class ThingsHandler extends BaseHandler { 678 | /** 679 | * Handle a GET request, including websocket requests. 680 | * 681 | * @param uriResource The URI resource that was matched 682 | * @param urlParams Map of URL parameters 683 | * @param session The HTTP session 684 | * @return The appropriate response. 685 | */ 686 | @Override 687 | public Response get(UriResource uriResource, 688 | Map urlParams, 689 | IHTTPSession session) { 690 | if (!validateHost(uriResource, session)) { 691 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN, 692 | null, 693 | null); 694 | } 695 | 696 | String wsHref = String.format("%s://%s", 697 | this.isSecure(uriResource) ? 698 | "wss" : 699 | "ws", 700 | session.getHeaders().get("host")); 701 | 702 | ThingsType things = uriResource.initParameter(0, ThingsType.class); 703 | 704 | JSONArray list = new JSONArray(); 705 | for (Thing thing : things.getThings()) { 706 | JSONObject description = thing.asThingDescription(); 707 | 708 | JSONObject link = new JSONObject(); 709 | link.put("rel", "alternate"); 710 | link.put("href", 711 | String.format("%s%s", wsHref, thing.getHref())); 712 | description.getJSONArray("links").put(link); 713 | 714 | String base = String.format("%s://%s%s", 715 | this.isSecure(uriResource) ? 716 | "https" : 717 | "http", 718 | session.getHeaders().get("host"), 719 | thing.getHref()); 720 | description.put("href", thing.getHref()); 721 | description.put("base", base); 722 | JSONObject securityDefinitions = new JSONObject(); 723 | JSONObject nosecSc = new JSONObject(); 724 | nosecSc.put("scheme", "nosec"); 725 | securityDefinitions.put("nosec_sc", nosecSc); 726 | description.put("securityDefinitions", securityDefinitions); 727 | description.put("security", "nosec_sc"); 728 | 729 | list.put(description); 730 | } 731 | 732 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK, 733 | "application/json", 734 | list.toString())); 735 | } 736 | } 737 | 738 | /** 739 | * Handle a request to /. 740 | */ 741 | public static class ThingHandler extends BaseHandler { 742 | /** 743 | * Handle a GET request, including websocket requests. 744 | * 745 | * @param uriResource The URI resource that was matched 746 | * @param urlParams Map of URL parameters 747 | * @param session The HTTP session 748 | * @return The appropriate response. 749 | */ 750 | @Override 751 | public Response get(UriResource uriResource, 752 | Map urlParams, 753 | IHTTPSession session) { 754 | if (!validateHost(uriResource, session)) { 755 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN, 756 | null, 757 | null); 758 | } 759 | 760 | Thing thing = this.getThing(uriResource, session); 761 | if (thing == null) { 762 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 763 | null, 764 | null)); 765 | } 766 | 767 | Map headers = session.getHeaders(); 768 | if (isWebSocketRequested(session)) { 769 | if (!NanoWSD.HEADER_WEBSOCKET_VERSION_VALUE.equalsIgnoreCase( 770 | headers.get(NanoWSD.HEADER_WEBSOCKET_VERSION))) { 771 | return corsResponse(newFixedLengthResponse(Response.Status.BAD_REQUEST, 772 | NanoHTTPD.MIME_PLAINTEXT, 773 | "Invalid Websocket-Version " + 774 | headers.get( 775 | NanoWSD.HEADER_WEBSOCKET_VERSION))); 776 | } 777 | 778 | if (!headers.containsKey(NanoWSD.HEADER_WEBSOCKET_KEY)) { 779 | return corsResponse(newFixedLengthResponse(Response.Status.BAD_REQUEST, 780 | NanoHTTPD.MIME_PLAINTEXT, 781 | "Missing Websocket-Key")); 782 | } 783 | 784 | final NanoWSD.WebSocket webSocket = 785 | new ThingWebSocket(thing, session); 786 | Response handshakeResponse = webSocket.getHandshakeResponse(); 787 | try { 788 | handshakeResponse.addHeader(NanoWSD.HEADER_WEBSOCKET_ACCEPT, 789 | NanoWSD.makeAcceptKey(headers.get( 790 | NanoWSD.HEADER_WEBSOCKET_KEY))); 791 | } catch (NoSuchAlgorithmException e) { 792 | return corsResponse(newFixedLengthResponse(Response.Status.INTERNAL_ERROR, 793 | NanoHTTPD.MIME_PLAINTEXT, 794 | "The SHA-1 Algorithm required for websockets is not available on the server.")); 795 | } 796 | 797 | if (headers.containsKey(NanoWSD.HEADER_WEBSOCKET_PROTOCOL)) { 798 | handshakeResponse.addHeader(NanoWSD.HEADER_WEBSOCKET_PROTOCOL, 799 | headers.get(NanoWSD.HEADER_WEBSOCKET_PROTOCOL) 800 | .split(",")[0]); 801 | } 802 | 803 | final Timer timer = new Timer(); 804 | timer.scheduleAtFixedRate(new TimerTask() { 805 | @Override 806 | public void run() { 807 | try { 808 | webSocket.ping(new byte[0]); 809 | } catch (IOException e) { 810 | timer.cancel(); 811 | } 812 | } 813 | }, 814 | WebThingServer.WEBSOCKET_PING_INTERVAL, 815 | WebThingServer.WEBSOCKET_PING_INTERVAL); 816 | 817 | return handshakeResponse; 818 | } 819 | 820 | String wsHref = String.format("%s://%s%s", 821 | this.isSecure(uriResource) ? 822 | "wss" : 823 | "ws", 824 | session.getHeaders().get("host"), 825 | thing.getHref()); 826 | JSONObject description = thing.asThingDescription(); 827 | JSONObject link = new JSONObject(); 828 | link.put("rel", "alternate"); 829 | link.put("href", wsHref); 830 | description.getJSONArray("links").put(link); 831 | 832 | String base = String.format("%s://%s%s", 833 | this.isSecure(uriResource) ? 834 | "https" : 835 | "http", 836 | session.getHeaders().get("host"), 837 | thing.getHref()); 838 | description.put("base", base); 839 | JSONObject securityDefinitions = new JSONObject(); 840 | JSONObject nosecSc = new JSONObject(); 841 | nosecSc.put("scheme", "nosec"); 842 | securityDefinitions.put("nosec_sc", nosecSc); 843 | description.put("securityDefinitions", securityDefinitions); 844 | description.put("security", "nosec_sc"); 845 | 846 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK, 847 | "application/json", 848 | description.toString())); 849 | } 850 | 851 | /** 852 | * Determine whether or not this is a websocket connection. 853 | * 854 | * @param headers The HTTP request headers 855 | * @return Boolean indicating whether or not this is a websocket 856 | * connection. 857 | */ 858 | private boolean isWebSocketConnectionHeader(Map headers) { 859 | String connection = headers.get(NanoWSD.HEADER_CONNECTION); 860 | return connection != null && connection.toLowerCase() 861 | .contains(NanoWSD.HEADER_CONNECTION_VALUE 862 | .toLowerCase()); 863 | } 864 | 865 | /** 866 | * Determine whether or not a websocket was requested. 867 | * 868 | * @param session The HTTP session 869 | * @return Boolean indicating whether or not this is a websocket 870 | * request. 871 | */ 872 | private boolean isWebSocketRequested(IHTTPSession session) { 873 | Map headers = session.getHeaders(); 874 | String upgrade = headers.get(NanoWSD.HEADER_UPGRADE); 875 | boolean isCorrectConnection = isWebSocketConnectionHeader(headers); 876 | boolean isUpgrade = 877 | NanoWSD.HEADER_UPGRADE_VALUE.equalsIgnoreCase(upgrade); 878 | return isUpgrade && isCorrectConnection; 879 | } 880 | 881 | /** 882 | * Class to handle WebSockets to a Thing. 883 | */ 884 | public static class ThingWebSocket extends NanoWSD.WebSocket { 885 | private final Thing thing; 886 | 887 | /** 888 | * Initialize the object. 889 | * 890 | * @param thing The Thing managed by the server 891 | * @param handshakeRequest The initial handshake request 892 | */ 893 | public ThingWebSocket(Thing thing, IHTTPSession handshakeRequest) { 894 | super(handshakeRequest); 895 | this.thing = thing; 896 | } 897 | 898 | /** 899 | * Handle a new connection. 900 | */ 901 | @Override 902 | protected void onOpen() { 903 | this.thing.addSubscriber(this); 904 | } 905 | 906 | /** 907 | * Handle a close event on the socket. 908 | * 909 | * @param code The close code 910 | * @param reason The close reason 911 | * @param initiatedByRemote Whether or not the client closed the 912 | * socket 913 | */ 914 | @Override 915 | protected void onClose(NanoWSD.WebSocketFrame.CloseCode code, 916 | String reason, 917 | boolean initiatedByRemote) { 918 | this.thing.removeSubscriber(this); 919 | } 920 | 921 | /** 922 | * Handle an incoming message. 923 | * 924 | * @param message The message to handle 925 | */ 926 | @Override 927 | protected void onMessage(NanoWSD.WebSocketFrame message) { 928 | message.setUnmasked(); 929 | String data = message.getTextPayload(); 930 | JSONObject json = new JSONObject(data); 931 | 932 | if (!json.has("messageType") || !json.has("data")) { 933 | JSONObject error = new JSONObject(); 934 | JSONObject inner = new JSONObject(); 935 | 936 | inner.put("status", "400 Bad Request"); 937 | inner.put("message", "Invalid message"); 938 | error.put("messageType", "error"); 939 | error.put("data", inner); 940 | 941 | this.sendMessage(error.toString()); 942 | 943 | return; 944 | } 945 | 946 | String messageType = json.getString("messageType"); 947 | JSONObject messageData = json.getJSONObject("data"); 948 | switch (messageType) { 949 | case "setProperty": 950 | JSONArray propertyNames = messageData.names(); 951 | if (propertyNames == null) { 952 | break; 953 | } 954 | 955 | for (int i = 0; i < propertyNames.length(); ++i) { 956 | String propertyName = propertyNames.getString(i); 957 | try { 958 | this.thing.setProperty(propertyName, 959 | messageData.get( 960 | propertyName)); 961 | } catch (PropertyError e) { 962 | JSONObject error = new JSONObject(); 963 | JSONObject inner = new JSONObject(); 964 | 965 | inner.put("status", "400 Bad Request"); 966 | inner.put("message", e.getMessage()); 967 | error.put("messageType", "error"); 968 | error.put("data", inner); 969 | 970 | this.sendMessage(e.getMessage()); 971 | } 972 | } 973 | break; 974 | case "requestAction": 975 | JSONArray actionNames = messageData.names(); 976 | if (actionNames == null) { 977 | break; 978 | } 979 | 980 | for (int i = 0; i < actionNames.length(); ++i) { 981 | String actionName = actionNames.getString(i); 982 | JSONObject params = 983 | messageData.getJSONObject(actionName); 984 | JSONObject input = null; 985 | if (params.has("input")) { 986 | input = params.getJSONObject("input"); 987 | } 988 | 989 | Action action = 990 | this.thing.performAction(actionName, input); 991 | if (action != null) { 992 | (new ActionRunner(action)).start(); 993 | } else { 994 | JSONObject error = new JSONObject(); 995 | JSONObject inner = new JSONObject(); 996 | 997 | inner.put("status", "400 Bad Request"); 998 | inner.put("message", "Invalid action request"); 999 | error.put("messageType", "error"); 1000 | error.put("data", inner); 1001 | 1002 | this.sendMessage(error.toString()); 1003 | } 1004 | } 1005 | break; 1006 | case "addEventSubscription": 1007 | JSONArray eventNames = messageData.names(); 1008 | if (eventNames == null) { 1009 | break; 1010 | } 1011 | 1012 | for (int i = 0; i < eventNames.length(); ++i) { 1013 | String eventName = eventNames.getString(i); 1014 | this.thing.addEventSubscriber(eventName, this); 1015 | } 1016 | break; 1017 | default: 1018 | JSONObject error = new JSONObject(); 1019 | JSONObject inner = new JSONObject(); 1020 | 1021 | inner.put("status", "400 Bad Request"); 1022 | inner.put("message", 1023 | "Unknown messageType: " + messageType); 1024 | error.put("messageType", "error"); 1025 | error.put("data", inner); 1026 | 1027 | this.sendMessage(error.toString()); 1028 | break; 1029 | } 1030 | } 1031 | 1032 | @Override 1033 | protected void onPong(NanoWSD.WebSocketFrame pong) { 1034 | } 1035 | 1036 | @Override 1037 | protected void onException(IOException exception) { 1038 | } 1039 | 1040 | public void sendMessage(String message) { 1041 | try { 1042 | this.send(message); 1043 | } catch (IOException e) { 1044 | // pass 1045 | } 1046 | } 1047 | } 1048 | } 1049 | 1050 | /** 1051 | * Handle a request to /properties. 1052 | */ 1053 | public static class PropertiesHandler extends BaseHandler { 1054 | /** 1055 | * Handle a GET request. 1056 | * 1057 | * @param uriResource The URI resource that was matched 1058 | * @param urlParams Map of URL parameters 1059 | * @param session The HTTP session 1060 | * @return The appropriate response. 1061 | */ 1062 | @Override 1063 | public Response get(UriResource uriResource, 1064 | Map urlParams, 1065 | IHTTPSession session) { 1066 | if (!validateHost(uriResource, session)) { 1067 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN, 1068 | null, 1069 | null); 1070 | } 1071 | 1072 | Thing thing = this.getThing(uriResource, session); 1073 | if (thing == null) { 1074 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1075 | null, 1076 | null)); 1077 | } 1078 | 1079 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK, 1080 | "application/json", 1081 | thing.getProperties() 1082 | .toString())); 1083 | } 1084 | } 1085 | 1086 | /** 1087 | * Handle a request to /properties/<property>. 1088 | */ 1089 | public static class PropertyHandler extends BaseHandler { 1090 | /** 1091 | * Get the property name from the URI. 1092 | * 1093 | * @param uriResource The URI resource that was matched 1094 | * @param session The HTTP session 1095 | * @return The property name. 1096 | */ 1097 | public String getPropertyName(UriResource uriResource, 1098 | IHTTPSession session) { 1099 | ThingsType things = uriResource.initParameter(0, ThingsType.class); 1100 | 1101 | if (MultipleThings.class.isInstance(things)) { 1102 | return this.getUriParam(session.getUri(), 3); 1103 | } else { 1104 | return this.getUriParam(session.getUri(), 2); 1105 | } 1106 | } 1107 | 1108 | /** 1109 | * Handle a GET request. 1110 | * 1111 | * @param uriResource The URI resource that was matched 1112 | * @param urlParams Map of URL parameters 1113 | * @param session The HTTP session 1114 | * @return The appropriate response. 1115 | */ 1116 | @Override 1117 | public Response get(UriResource uriResource, 1118 | Map urlParams, 1119 | IHTTPSession session) { 1120 | if (!validateHost(uriResource, session)) { 1121 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN, 1122 | null, 1123 | null); 1124 | } 1125 | 1126 | Thing thing = this.getThing(uriResource, session); 1127 | if (thing == null) { 1128 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1129 | null, 1130 | null)); 1131 | } 1132 | 1133 | String propertyName = this.getPropertyName(uriResource, session); 1134 | if (!thing.hasProperty(propertyName)) { 1135 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1136 | null, 1137 | null)); 1138 | } 1139 | 1140 | JSONObject obj = new JSONObject(); 1141 | try { 1142 | Object value = thing.getProperty(propertyName); 1143 | if (value == null) { 1144 | obj.put(propertyName, JSONObject.NULL); 1145 | } else { 1146 | obj.putOpt(propertyName, value); 1147 | } 1148 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK, 1149 | "application/json", 1150 | obj.toString())); 1151 | } catch (JSONException e) { 1152 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.INTERNAL_ERROR, 1153 | null, 1154 | null)); 1155 | } 1156 | } 1157 | 1158 | /** 1159 | * Handle a PUT request. 1160 | * 1161 | * @param uriResource The URI resource that was matched 1162 | * @param urlParams Map of URL parameters 1163 | * @param session The HTTP session 1164 | * @return The appropriate response. 1165 | */ 1166 | @Override 1167 | public Response put(UriResource uriResource, 1168 | Map urlParams, 1169 | IHTTPSession session) { 1170 | if (!validateHost(uriResource, session)) { 1171 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN, 1172 | null, 1173 | null); 1174 | } 1175 | 1176 | Thing thing = this.getThing(uriResource, session); 1177 | if (thing == null) { 1178 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1179 | null, 1180 | null)); 1181 | } 1182 | 1183 | String propertyName = this.getPropertyName(uriResource, session); 1184 | if (!thing.hasProperty(propertyName)) { 1185 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1186 | null, 1187 | null)); 1188 | } 1189 | 1190 | JSONObject json = this.parseBody(session); 1191 | if (json == null) { 1192 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.BAD_REQUEST, 1193 | null, 1194 | null)); 1195 | } 1196 | 1197 | if (!json.has(propertyName)) { 1198 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.BAD_REQUEST, 1199 | null, 1200 | null)); 1201 | } 1202 | 1203 | try { 1204 | thing.setProperty(propertyName, json.get(propertyName)); 1205 | 1206 | JSONObject obj = new JSONObject(); 1207 | obj.putOpt(propertyName, thing.getProperty(propertyName)); 1208 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK, 1209 | "application/json", 1210 | obj.toString())); 1211 | } catch (JSONException e) { 1212 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.INTERNAL_ERROR, 1213 | null, 1214 | null)); 1215 | } catch (PropertyError e) { 1216 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.BAD_REQUEST, 1217 | null, 1218 | null)); 1219 | } 1220 | } 1221 | } 1222 | 1223 | /** 1224 | * Handle a request to /actions. 1225 | */ 1226 | public static class ActionsHandler extends BaseHandler { 1227 | /** 1228 | * Handle a GET request. 1229 | * 1230 | * @param uriResource The URI resource that was matched 1231 | * @param urlParams Map of URL parameters 1232 | * @param session The HTTP session 1233 | * @return The appropriate response. 1234 | */ 1235 | @Override 1236 | public Response get(UriResource uriResource, 1237 | Map urlParams, 1238 | IHTTPSession session) { 1239 | if (!validateHost(uriResource, session)) { 1240 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN, 1241 | null, 1242 | null); 1243 | } 1244 | 1245 | Thing thing = this.getThing(uriResource, session); 1246 | if (thing == null) { 1247 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1248 | null, 1249 | null)); 1250 | } 1251 | 1252 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK, 1253 | "application/json", 1254 | thing.getActionDescriptions( 1255 | null) 1256 | .toString())); 1257 | } 1258 | 1259 | /** 1260 | * Handle a POST request. 1261 | * 1262 | * @param uriResource The URI resource that was matched 1263 | * @param urlParams Map of URL parameters 1264 | * @param session The HTTP session 1265 | * @return The appropriate response. 1266 | */ 1267 | @Override 1268 | public Response post(UriResource uriResource, 1269 | Map urlParams, 1270 | IHTTPSession session) { 1271 | if (!validateHost(uriResource, session)) { 1272 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN, 1273 | null, 1274 | null); 1275 | } 1276 | 1277 | Thing thing = this.getThing(uriResource, session); 1278 | if (thing == null) { 1279 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1280 | null, 1281 | null)); 1282 | } 1283 | 1284 | JSONObject json = this.parseBody(session); 1285 | if (json == null) { 1286 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.BAD_REQUEST, 1287 | null, 1288 | null)); 1289 | } 1290 | 1291 | try { 1292 | JSONArray actionNames = json.names(); 1293 | if (actionNames == null || actionNames.length() != 1) { 1294 | return corsResponse(NanoHTTPD.newFixedLengthResponse( 1295 | Response.Status.BAD_REQUEST, 1296 | null, 1297 | null)); 1298 | } 1299 | 1300 | String actionName = actionNames.getString(0); 1301 | JSONObject params = json.getJSONObject(actionName); 1302 | JSONObject input = null; 1303 | if (params.has("input")) { 1304 | input = params.getJSONObject("input"); 1305 | } 1306 | 1307 | Action action = thing.performAction(actionName, input); 1308 | if (action != null) { 1309 | JSONObject response = new JSONObject(); 1310 | response.put(actionName, 1311 | action.asActionDescription() 1312 | .getJSONObject(actionName)); 1313 | 1314 | (new ActionRunner(action)).start(); 1315 | 1316 | return corsResponse(NanoHTTPD.newFixedLengthResponse( 1317 | Response.Status.CREATED, 1318 | "application/json", 1319 | response.toString())); 1320 | } else { 1321 | return corsResponse(NanoHTTPD.newFixedLengthResponse( 1322 | Response.Status.BAD_REQUEST, 1323 | null, 1324 | null)); 1325 | } 1326 | } catch (JSONException e) { 1327 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.INTERNAL_ERROR, 1328 | null, 1329 | null)); 1330 | } 1331 | } 1332 | } 1333 | 1334 | /** 1335 | * Handle a request to /actions/<action_name>. 1336 | */ 1337 | public static class ActionHandler extends BaseHandler { 1338 | /** 1339 | * Get the action name from the URI. 1340 | * 1341 | * @param uriResource The URI resource that was matched 1342 | * @param session The HTTP session 1343 | * @return The property name. 1344 | */ 1345 | public String getActionName(UriResource uriResource, 1346 | IHTTPSession session) { 1347 | ThingsType things = uriResource.initParameter(0, ThingsType.class); 1348 | 1349 | if (MultipleThings.class.isInstance(things)) { 1350 | return this.getUriParam(session.getUri(), 3); 1351 | } else { 1352 | return this.getUriParam(session.getUri(), 2); 1353 | } 1354 | } 1355 | 1356 | /** 1357 | * Handle a GET request. 1358 | * 1359 | * @param uriResource The URI resource that was matched 1360 | * @param urlParams Map of URL parameters 1361 | * @param session The HTTP session 1362 | * @return The appropriate response. 1363 | */ 1364 | @Override 1365 | public Response get(UriResource uriResource, 1366 | Map urlParams, 1367 | IHTTPSession session) { 1368 | if (!validateHost(uriResource, session)) { 1369 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN, 1370 | null, 1371 | null); 1372 | } 1373 | 1374 | Thing thing = this.getThing(uriResource, session); 1375 | if (thing == null) { 1376 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1377 | null, 1378 | null)); 1379 | } 1380 | 1381 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK, 1382 | "application/json", 1383 | thing.getActionDescriptions( 1384 | this.getActionName( 1385 | uriResource, 1386 | session)) 1387 | .toString())); 1388 | } 1389 | 1390 | /** 1391 | * Handle a POST request. 1392 | * 1393 | * @param uriResource The URI resource that was matched 1394 | * @param urlParams Map of URL parameters 1395 | * @param session The HTTP session 1396 | * @return The appropriate response. 1397 | */ 1398 | @Override 1399 | public Response post(UriResource uriResource, 1400 | Map urlParams, 1401 | IHTTPSession session) { 1402 | if (!validateHost(uriResource, session)) { 1403 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN, 1404 | null, 1405 | null); 1406 | } 1407 | 1408 | Thing thing = this.getThing(uriResource, session); 1409 | if (thing == null) { 1410 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1411 | null, 1412 | null)); 1413 | } 1414 | 1415 | JSONObject json = this.parseBody(session); 1416 | if (json == null) { 1417 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.BAD_REQUEST, 1418 | null, 1419 | null)); 1420 | } 1421 | 1422 | String actionName = this.getActionName(uriResource, session); 1423 | 1424 | try { 1425 | JSONArray actionNames = json.names(); 1426 | if (actionNames == null || actionNames.length() != 1) { 1427 | return corsResponse(NanoHTTPD.newFixedLengthResponse( 1428 | Response.Status.BAD_REQUEST, 1429 | null, 1430 | null)); 1431 | } 1432 | 1433 | String name = actionNames.getString(0); 1434 | if (!name.equals(actionName)) { 1435 | return corsResponse(NanoHTTPD.newFixedLengthResponse( 1436 | Response.Status.BAD_REQUEST, 1437 | null, 1438 | null)); 1439 | } 1440 | 1441 | JSONObject params = json.getJSONObject(name); 1442 | JSONObject input = null; 1443 | if (params.has("input")) { 1444 | input = params.getJSONObject("input"); 1445 | } 1446 | 1447 | Action action = thing.performAction(name, input); 1448 | if (action != null) { 1449 | JSONObject response = new JSONObject(); 1450 | response.put(name, 1451 | action.asActionDescription() 1452 | .getJSONObject(name)); 1453 | 1454 | (new ActionRunner(action)).start(); 1455 | 1456 | return corsResponse(NanoHTTPD.newFixedLengthResponse( 1457 | Response.Status.CREATED, 1458 | "application/json", 1459 | response.toString())); 1460 | } else { 1461 | return corsResponse(NanoHTTPD.newFixedLengthResponse( 1462 | Response.Status.BAD_REQUEST, 1463 | null, 1464 | null)); 1465 | } 1466 | } catch (JSONException e) { 1467 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.INTERNAL_ERROR, 1468 | null, 1469 | null)); 1470 | } 1471 | } 1472 | } 1473 | 1474 | /** 1475 | * Handle a request to /actions/<action_name>/<action_id>. 1476 | */ 1477 | public static class ActionIDHandler extends BaseHandler { 1478 | /** 1479 | * Get the action name from the URI. 1480 | * 1481 | * @param uriResource The URI resource that was matched 1482 | * @param session The HTTP session 1483 | * @return The property name. 1484 | */ 1485 | public String getActionName(UriResource uriResource, 1486 | IHTTPSession session) { 1487 | ThingsType things = uriResource.initParameter(0, ThingsType.class); 1488 | 1489 | if (MultipleThings.class.isInstance(things)) { 1490 | return this.getUriParam(session.getUri(), 3); 1491 | } else { 1492 | return this.getUriParam(session.getUri(), 2); 1493 | } 1494 | } 1495 | 1496 | /** 1497 | * Get the action ID from the URI. 1498 | * 1499 | * @param uriResource The URI resource that was matched 1500 | * @param session The HTTP session 1501 | * @return The property name. 1502 | */ 1503 | public String getActionId(UriResource uriResource, 1504 | IHTTPSession session) { 1505 | ThingsType things = uriResource.initParameter(0, ThingsType.class); 1506 | 1507 | if (MultipleThings.class.isInstance(things)) { 1508 | return this.getUriParam(session.getUri(), 4); 1509 | } else { 1510 | return this.getUriParam(session.getUri(), 3); 1511 | } 1512 | } 1513 | 1514 | /** 1515 | * Handle a GET request. 1516 | * 1517 | * @param uriResource The URI resource that was matched 1518 | * @param urlParams Map of URL parameters 1519 | * @param session The HTTP session 1520 | * @return The appropriate response. 1521 | */ 1522 | @Override 1523 | public Response get(UriResource uriResource, 1524 | Map urlParams, 1525 | IHTTPSession session) { 1526 | if (!validateHost(uriResource, session)) { 1527 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN, 1528 | null, 1529 | null); 1530 | } 1531 | 1532 | Thing thing = this.getThing(uriResource, session); 1533 | if (thing == null) { 1534 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1535 | null, 1536 | null)); 1537 | } 1538 | 1539 | String actionName = this.getActionName(uriResource, session); 1540 | String actionId = this.getActionId(uriResource, session); 1541 | 1542 | Action action = thing.getAction(actionName, actionId); 1543 | if (action == null) { 1544 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1545 | null, 1546 | null)); 1547 | } 1548 | 1549 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK, 1550 | "application/json", 1551 | action.asActionDescription() 1552 | .toString())); 1553 | } 1554 | 1555 | /** 1556 | * Handle a PUT request. 1557 | * 1558 | * @param uriResource The URI resource that was matched 1559 | * @param urlParams Map of URL parameters 1560 | * @param session The HTTP session 1561 | * @return The appropriate response. 1562 | */ 1563 | @Override 1564 | public Response put(UriResource uriResource, 1565 | Map urlParams, 1566 | IHTTPSession session) { 1567 | if (!validateHost(uriResource, session)) { 1568 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN, 1569 | null, 1570 | null); 1571 | } 1572 | 1573 | Thing thing = this.getThing(uriResource, session); 1574 | if (thing == null) { 1575 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1576 | null, 1577 | null)); 1578 | } 1579 | 1580 | // TODO: this is not yet defined in the spec 1581 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK, 1582 | "application/json", 1583 | "")); 1584 | } 1585 | 1586 | /** 1587 | * Handle a DELETE request. 1588 | * 1589 | * @param uriResource The URI resource that was matched 1590 | * @param urlParams Map of URL parameters 1591 | * @param session The HTTP session 1592 | * @return The appropriate response. 1593 | */ 1594 | @Override 1595 | public Response delete(UriResource uriResource, 1596 | Map urlParams, 1597 | IHTTPSession session) { 1598 | if (!validateHost(uriResource, session)) { 1599 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN, 1600 | null, 1601 | null); 1602 | } 1603 | 1604 | Thing thing = this.getThing(uriResource, session); 1605 | if (thing == null) { 1606 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1607 | null, 1608 | null)); 1609 | } 1610 | 1611 | String actionName = this.getActionName(uriResource, session); 1612 | String actionId = this.getActionId(uriResource, session); 1613 | 1614 | if (thing.removeAction(actionName, actionId)) { 1615 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NO_CONTENT, 1616 | null, 1617 | null)); 1618 | } else { 1619 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1620 | null, 1621 | null)); 1622 | } 1623 | } 1624 | } 1625 | 1626 | /** 1627 | * Handle a request to /events. 1628 | */ 1629 | public static class EventsHandler extends BaseHandler { 1630 | /** 1631 | * Handle a GET request. 1632 | * 1633 | * @param uriResource The URI resource that was matched 1634 | * @param urlParams Map of URL parameters 1635 | * @param session The HTTP session 1636 | * @return The appropriate response. 1637 | */ 1638 | @Override 1639 | public Response get(UriResource uriResource, 1640 | Map urlParams, 1641 | IHTTPSession session) { 1642 | if (!validateHost(uriResource, session)) { 1643 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN, 1644 | null, 1645 | null); 1646 | } 1647 | 1648 | Thing thing = this.getThing(uriResource, session); 1649 | if (thing == null) { 1650 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1651 | null, 1652 | null)); 1653 | } 1654 | 1655 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK, 1656 | "application/json", 1657 | thing.getEventDescriptions( 1658 | null) 1659 | .toString())); 1660 | } 1661 | } 1662 | 1663 | /** 1664 | * Handle a request to /events/<event_name>. 1665 | */ 1666 | public static class EventHandler extends BaseHandler { 1667 | /** 1668 | * Get the event name from the URI. 1669 | * 1670 | * @param uriResource The URI resource that was matched 1671 | * @param session The HTTP session 1672 | * @return The property name. 1673 | */ 1674 | public String getEventName(UriResource uriResource, 1675 | IHTTPSession session) { 1676 | ThingsType things = uriResource.initParameter(0, ThingsType.class); 1677 | 1678 | if (MultipleThings.class.isInstance(things)) { 1679 | return this.getUriParam(session.getUri(), 3); 1680 | } else { 1681 | return this.getUriParam(session.getUri(), 2); 1682 | } 1683 | } 1684 | 1685 | /** 1686 | * Handle a GET request. 1687 | * 1688 | * @param uriResource The URI resource that was matched 1689 | * @param urlParams Map of URL parameters 1690 | * @param session The HTTP session 1691 | * @return The appropriate response. 1692 | */ 1693 | @Override 1694 | public Response get(UriResource uriResource, 1695 | Map urlParams, 1696 | IHTTPSession session) { 1697 | if (!validateHost(uriResource, session)) { 1698 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN, 1699 | null, 1700 | null); 1701 | } 1702 | 1703 | Thing thing = this.getThing(uriResource, session); 1704 | if (thing == null) { 1705 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, 1706 | null, 1707 | null)); 1708 | } 1709 | 1710 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK, 1711 | "application/json", 1712 | thing.getEventDescriptions( 1713 | this.getEventName( 1714 | uriResource, 1715 | session)) 1716 | .toString())); 1717 | } 1718 | } 1719 | 1720 | /** 1721 | * A container for a single thing. 1722 | */ 1723 | public static class SingleThing implements ThingsType { 1724 | private final Thing thing; 1725 | 1726 | /** 1727 | * Initialize the container. 1728 | * 1729 | * @param thing The thing to store 1730 | */ 1731 | public SingleThing(Thing thing) { 1732 | this.thing = thing; 1733 | } 1734 | 1735 | /** 1736 | * Get the thing at the given index. 1737 | * 1738 | * @param idx The index. 1739 | */ 1740 | public Thing getThing(int idx) { 1741 | return this.thing; 1742 | } 1743 | 1744 | /** 1745 | * Get the list of things. 1746 | * 1747 | * @return The list of things. 1748 | */ 1749 | public List getThings() { 1750 | List things = new ArrayList<>(); 1751 | things.add(this.thing); 1752 | return things; 1753 | } 1754 | 1755 | /** 1756 | * Get the mDNS server name. 1757 | * 1758 | * @return The server name. 1759 | */ 1760 | public String getName() { 1761 | return this.thing.getTitle(); 1762 | } 1763 | } 1764 | 1765 | /** 1766 | * A container for multiple things. 1767 | */ 1768 | public static class MultipleThings implements ThingsType { 1769 | private final List things; 1770 | private final String name; 1771 | 1772 | /** 1773 | * Initialize the container. 1774 | * 1775 | * @param things The things to store 1776 | * @param name The mDNS server name 1777 | */ 1778 | public MultipleThings(List things, String name) { 1779 | this.things = things; 1780 | this.name = name; 1781 | } 1782 | 1783 | /** 1784 | * Get the thing at the given index. 1785 | * 1786 | * @param idx The index. 1787 | */ 1788 | public Thing getThing(int idx) { 1789 | if (idx < 0 || idx >= this.things.size()) { 1790 | return null; 1791 | } 1792 | 1793 | return this.things.get(idx); 1794 | } 1795 | 1796 | /** 1797 | * Get the list of things. 1798 | * 1799 | * @return The list of things. 1800 | */ 1801 | public List getThings() { 1802 | return this.things; 1803 | } 1804 | 1805 | /** 1806 | * Get the mDNS server name. 1807 | * 1808 | * @return The server name. 1809 | */ 1810 | public String getName() { 1811 | return this.name; 1812 | } 1813 | } 1814 | 1815 | /** 1816 | * Mini-class used to define additional API routes. 1817 | */ 1818 | public static class Route { 1819 | public String url; 1820 | public Class handlerClass; 1821 | public Object[] parameters; 1822 | 1823 | /** 1824 | * Initialize the new route. 1825 | *

1826 | * See: https://github.com/NanoHttpd/nanohttpd/blob/master/nanolets/src/main/java/org/nanohttpd/router/RouterNanoHTTPD.java 1827 | * 1828 | * @param url URL to match. 1829 | * @param handlerClass Class which will handle the request. 1830 | * @param parameters Initialization parameters for class instance. 1831 | */ 1832 | public Route(String url, Class handlerClass, Object... parameters) { 1833 | this.url = url; 1834 | this.handlerClass = handlerClass; 1835 | this.parameters = parameters; 1836 | } 1837 | } 1838 | } 1839 | -------------------------------------------------------------------------------- /src/main/java/io/webthings/webthing/errors/PropertyError.java: -------------------------------------------------------------------------------- 1 | package io.webthings.webthing.errors; 2 | 3 | public class PropertyError extends Exception { 4 | public PropertyError() { 5 | super("General property error"); 6 | } 7 | 8 | public PropertyError(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/webthings/webthing/example/MultipleThings.java: -------------------------------------------------------------------------------- 1 | package io.webthings.webthing.example; 2 | 3 | import org.json.JSONArray; 4 | import org.json.JSONObject; 5 | 6 | import java.io.IOException; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.UUID; 11 | 12 | import io.webthings.webthing.Action; 13 | import io.webthings.webthing.Event; 14 | import io.webthings.webthing.Property; 15 | import io.webthings.webthing.Thing; 16 | import io.webthings.webthing.Value; 17 | import io.webthings.webthing.WebThingServer; 18 | import io.webthings.webthing.errors.PropertyError; 19 | 20 | public class MultipleThings { 21 | public static void main(String[] args) { 22 | // Create a thing that represents a dimmable light 23 | Thing light = new ExampleDimmableLight(); 24 | 25 | // Create a thing that represents a humidity sensor 26 | Thing sensor = new FakeGpioHumiditySensor(); 27 | 28 | try { 29 | List things = new ArrayList<>(); 30 | things.add(light); 31 | things.add(sensor); 32 | 33 | // If adding more than one thing, use MultipleThings() with a name. 34 | // In the single thing case, the thing's name will be broadcast. 35 | WebThingServer server = 36 | new WebThingServer(new WebThingServer.MultipleThings(things, 37 | "LightAndTempDevice"), 38 | 8888); 39 | 40 | Runtime.getRuntime().addShutdownHook(new Thread(server::stop)); 41 | 42 | server.start(false); 43 | } catch (IOException e) { 44 | System.out.println(e.toString()); 45 | System.exit(1); 46 | } 47 | } 48 | 49 | /** 50 | * A dimmable light that logs received commands to std::out. 51 | */ 52 | public static class ExampleDimmableLight extends Thing { 53 | public ExampleDimmableLight() { 54 | super("urn:dev:ops:my-lamp-1234", 55 | "My Lamp", 56 | new JSONArray(Arrays.asList("OnOffSwitch", "Light")), 57 | "A web connected lamp"); 58 | 59 | JSONObject onDescription = new JSONObject(); 60 | onDescription.put("@type", "OnOffProperty"); 61 | onDescription.put("title", "On/Off"); 62 | onDescription.put("type", "boolean"); 63 | onDescription.put("description", "Whether the lamp is turned on"); 64 | 65 | Value on = new Value<>(true, 66 | // Here, you could send a signal to 67 | // the GPIO that switches the lamp 68 | // off 69 | v -> System.out.printf( 70 | "On-State is now %s\n", 71 | v)); 72 | 73 | this.addProperty(new Property(this, "on", on, onDescription)); 74 | 75 | JSONObject brightnessDescription = new JSONObject(); 76 | brightnessDescription.put("@type", "BrightnessProperty"); 77 | brightnessDescription.put("title", "Brightness"); 78 | brightnessDescription.put("type", "integer"); 79 | brightnessDescription.put("description", 80 | "The level of light from 0-100"); 81 | brightnessDescription.put("minimum", 0); 82 | brightnessDescription.put("maximum", 100); 83 | brightnessDescription.put("unit", "percent"); 84 | 85 | Value brightness = new Value<>(50, 86 | // Here, you could send a signal 87 | // to the GPIO that controls the 88 | // brightness 89 | l -> System.out.printf( 90 | "Brightness is now %s\n", 91 | l)); 92 | 93 | this.addProperty(new Property(this, 94 | "brightness", 95 | brightness, 96 | brightnessDescription)); 97 | 98 | JSONObject fadeMetadata = new JSONObject(); 99 | JSONObject fadeInput = new JSONObject(); 100 | JSONObject fadeProperties = new JSONObject(); 101 | JSONObject fadeBrightness = new JSONObject(); 102 | JSONObject fadeDuration = new JSONObject(); 103 | fadeMetadata.put("title", "Fade"); 104 | fadeMetadata.put("description", "Fade the lamp to a given level"); 105 | fadeInput.put("type", "object"); 106 | fadeInput.put("required", 107 | new JSONArray(Arrays.asList("brightness", 108 | "duration"))); 109 | fadeBrightness.put("type", "integer"); 110 | fadeBrightness.put("minimum", 0); 111 | fadeBrightness.put("maximum", 100); 112 | fadeBrightness.put("unit", "percent"); 113 | fadeDuration.put("type", "integer"); 114 | fadeDuration.put("minimum", 1); 115 | fadeDuration.put("unit", "milliseconds"); 116 | fadeProperties.put("brightness", fadeBrightness); 117 | fadeProperties.put("duration", fadeDuration); 118 | fadeInput.put("properties", fadeProperties); 119 | fadeMetadata.put("input", fadeInput); 120 | this.addAvailableAction("fade", fadeMetadata, FadeAction.class); 121 | 122 | JSONObject overheatedMetadata = new JSONObject(); 123 | overheatedMetadata.put("description", 124 | "The lamp has exceeded its safe operating temperature"); 125 | overheatedMetadata.put("type", "number"); 126 | overheatedMetadata.put("unit", "degree celsius"); 127 | this.addAvailableEvent("overheated", overheatedMetadata); 128 | } 129 | 130 | public static class OverheatedEvent extends Event { 131 | public OverheatedEvent(Thing thing, int data) { 132 | super(thing, "overheated", data); 133 | } 134 | } 135 | 136 | public static class FadeAction extends Action { 137 | public FadeAction(Thing thing, JSONObject input) { 138 | super(UUID.randomUUID().toString(), thing, "fade", input); 139 | } 140 | 141 | @Override 142 | public void performAction() { 143 | Thing thing = this.getThing(); 144 | JSONObject input = this.getInput(); 145 | try { 146 | Thread.sleep(input.getInt("duration")); 147 | } catch (InterruptedException e) { 148 | // pass 149 | } 150 | 151 | try { 152 | thing.setProperty("brightness", input.getInt("brightness")); 153 | thing.addEvent(new OverheatedEvent(thing, 102)); 154 | } catch (PropertyError e) { 155 | // pass 156 | } 157 | } 158 | } 159 | } 160 | 161 | /** 162 | * A humidity sensor which updates its measurement every few seconds. 163 | */ 164 | public static class FakeGpioHumiditySensor extends Thing { 165 | private final Value level; 166 | 167 | public FakeGpioHumiditySensor() { 168 | super("urn:dev:ops:my-humidity-sensor-1234", 169 | "My Humidity Sensor", 170 | new JSONArray(Arrays.asList("MultiLevelSensor")), 171 | "A web connected humidity sensor"); 172 | 173 | JSONObject levelDescription = new JSONObject(); 174 | levelDescription.put("@type", "LevelProperty"); 175 | levelDescription.put("title", "Humidity"); 176 | levelDescription.put("type", "number"); 177 | levelDescription.put("description", "The current humidity in %"); 178 | levelDescription.put("minimum", 0); 179 | levelDescription.put("maximum", 100); 180 | levelDescription.put("unit", "percent"); 181 | levelDescription.put("readOnly", true); 182 | this.level = new Value<>(0.0); 183 | this.addProperty(new Property(this, 184 | "level", 185 | level, 186 | levelDescription)); 187 | 188 | // Start a thread that polls the sensor reading every 3 seconds 189 | new Thread(() -> { 190 | while (true) { 191 | try { 192 | Thread.sleep(3000); 193 | // Update the underlying value, which in turn notifies 194 | // all listeners 195 | double newLevel = this.readFromGPIO(); 196 | System.out.printf("setting new humidity level: %f\n", 197 | newLevel); 198 | this.level.notifyOfExternalUpdate(newLevel); 199 | } catch (InterruptedException e) { 200 | throw new IllegalStateException(e); 201 | } 202 | } 203 | }).start(); 204 | } 205 | 206 | /** 207 | * Mimic an actual sensor updating its reading every couple seconds. 208 | */ 209 | private double readFromGPIO() { 210 | return Math.abs(70.0d * Math.random() * (-0.5 + Math.random())); 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/main/java/io/webthings/webthing/example/SingleThing.java: -------------------------------------------------------------------------------- 1 | package io.webthings.webthing.example; 2 | 3 | import org.json.JSONArray; 4 | import org.json.JSONObject; 5 | 6 | import java.io.IOException; 7 | import java.util.Arrays; 8 | import java.util.UUID; 9 | 10 | import io.webthings.webthing.Action; 11 | import io.webthings.webthing.Event; 12 | import io.webthings.webthing.Property; 13 | import io.webthings.webthing.Thing; 14 | import io.webthings.webthing.Value; 15 | import io.webthings.webthing.WebThingServer; 16 | import io.webthings.webthing.errors.PropertyError; 17 | 18 | public class SingleThing { 19 | public static Thing makeThing() { 20 | Thing thing = new Thing("urn:dev:ops:my-lamp-1234", 21 | "My Lamp", 22 | new JSONArray(Arrays.asList("OnOffSwitch", 23 | "Light")), 24 | "A web connected lamp"); 25 | 26 | JSONObject onDescription = new JSONObject(); 27 | onDescription.put("@type", "OnOffProperty"); 28 | onDescription.put("title", "On/Off"); 29 | onDescription.put("type", "boolean"); 30 | onDescription.put("description", "Whether the lamp is turned on"); 31 | thing.addProperty(new Property(thing, 32 | "on", 33 | new Value(true), 34 | onDescription)); 35 | 36 | JSONObject brightnessDescription = new JSONObject(); 37 | brightnessDescription.put("@type", "BrightnessProperty"); 38 | brightnessDescription.put("title", "Brightness"); 39 | brightnessDescription.put("type", "integer"); 40 | brightnessDescription.put("description", 41 | "The level of light from 0-100"); 42 | brightnessDescription.put("minimum", 0); 43 | brightnessDescription.put("maximum", 100); 44 | brightnessDescription.put("unit", "percent"); 45 | thing.addProperty(new Property(thing, 46 | "brightness", 47 | new Value(50), 48 | brightnessDescription)); 49 | 50 | JSONObject fadeMetadata = new JSONObject(); 51 | JSONObject fadeInput = new JSONObject(); 52 | JSONObject fadeProperties = new JSONObject(); 53 | JSONObject fadeBrightness = new JSONObject(); 54 | JSONObject fadeDuration = new JSONObject(); 55 | fadeMetadata.put("title", "Fade"); 56 | fadeMetadata.put("description", "Fade the lamp to a given level"); 57 | fadeInput.put("type", "object"); 58 | fadeInput.put("required", 59 | new JSONArray(Arrays.asList("brightness", "duration"))); 60 | fadeBrightness.put("type", "integer"); 61 | fadeBrightness.put("minimum", 0); 62 | fadeBrightness.put("maximum", 100); 63 | fadeBrightness.put("unit", "percent"); 64 | fadeDuration.put("type", "integer"); 65 | fadeDuration.put("minimum", 1); 66 | fadeDuration.put("unit", "milliseconds"); 67 | fadeProperties.put("brightness", fadeBrightness); 68 | fadeProperties.put("duration", fadeDuration); 69 | fadeInput.put("properties", fadeProperties); 70 | fadeMetadata.put("input", fadeInput); 71 | thing.addAvailableAction("fade", fadeMetadata, FadeAction.class); 72 | 73 | JSONObject overheatedMetadata = new JSONObject(); 74 | overheatedMetadata.put("description", 75 | "The lamp has exceeded its safe operating temperature"); 76 | overheatedMetadata.put("type", "number"); 77 | overheatedMetadata.put("unit", "degree celsius"); 78 | thing.addAvailableEvent("overheated", overheatedMetadata); 79 | 80 | return thing; 81 | } 82 | 83 | public static void main(String[] args) { 84 | Thing thing = makeThing(); 85 | WebThingServer server; 86 | 87 | try { 88 | // If adding more than one thing, use MultipleThings() with a name. 89 | // In the single thing case, the thing's name will be broadcast. 90 | server = new WebThingServer(new WebThingServer.SingleThing(thing), 91 | 8888); 92 | 93 | Runtime.getRuntime().addShutdownHook(new Thread(server::stop)); 94 | 95 | server.start(false); 96 | } catch (IOException e) { 97 | System.out.println(e.toString()); 98 | System.exit(1); 99 | } 100 | } 101 | 102 | public static class OverheatedEvent extends Event { 103 | public OverheatedEvent(Thing thing, int data) { 104 | super(thing, "overheated", data); 105 | } 106 | } 107 | 108 | public static class FadeAction extends Action { 109 | public FadeAction(Thing thing, JSONObject input) { 110 | super(UUID.randomUUID().toString(), thing, "fade", input); 111 | } 112 | 113 | @Override 114 | public void performAction() { 115 | Thing thing = this.getThing(); 116 | JSONObject input = this.getInput(); 117 | try { 118 | Thread.sleep(input.getInt("duration")); 119 | } catch (InterruptedException e) { 120 | // pass 121 | } 122 | 123 | try { 124 | thing.setProperty("brightness", input.getInt("brightness")); 125 | thing.addEvent(new OverheatedEvent(thing, 102)); 126 | } catch (PropertyError e) { 127 | // pass 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/test/java/io/webthings/webthing/ThingTest.java: -------------------------------------------------------------------------------- 1 | package io.webthings.webthing; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertFalse; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | import java.util.Arrays; 8 | import java.util.Collections; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | import org.json.JSONArray; 13 | import org.json.JSONObject; 14 | import org.junit.Test; 15 | 16 | import io.webthings.webthing.errors.PropertyError; 17 | 18 | public class ThingTest { 19 | 20 | Object simulateHttpPutProperty(String key, String jsonBody) { 21 | JSONObject json = new JSONObject(jsonBody); 22 | return json.get(key); 23 | } 24 | 25 | @Test 26 | public void itSupportsIntegerValues() throws PropertyError 27 | { 28 | // given 29 | Thing thing = new Thing("urn:dev:test-123", "My TestThing"); 30 | 31 | Value value = new Value<>(42, v -> System.out.println("value: " + v)); 32 | thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "integer"))); 33 | 34 | // when updating property, then 35 | assertEquals(42, value.get().intValue()); 36 | 37 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Integer.MIN_VALUE+"}")); 38 | assertEquals(Integer.MIN_VALUE, value.get().intValue()); 39 | 40 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Integer.MAX_VALUE+"}")); 41 | assertEquals(Integer.MAX_VALUE, value.get().intValue()); 42 | 43 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4.2}")); 44 | assertEquals(4, value.get().intValue()); 45 | 46 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4}")); 47 | assertEquals(4, value.get().intValue()); 48 | 49 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":0}")); 50 | assertEquals(0, value.get().intValue()); 51 | 52 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":-123}")); 53 | assertEquals(-123, value.get().intValue()); 54 | } 55 | 56 | @Test 57 | public void itSupportsLongValues() throws PropertyError 58 | { 59 | // given 60 | Thing thing = new Thing("urn:dev:test-123", "My TestThing"); 61 | 62 | Value value = new Value<>(42l, v -> System.out.println("value: " + v)); 63 | thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "integer"))); 64 | 65 | // when updating property, then 66 | assertEquals(42, value.get().longValue()); 67 | 68 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Long.MIN_VALUE+"}")); 69 | assertEquals(Long.MIN_VALUE, value.get().longValue()); 70 | 71 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Long.MAX_VALUE+"}")); 72 | assertEquals(Long.MAX_VALUE, value.get().longValue()); 73 | 74 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4.2}")); 75 | assertEquals(4, value.get().longValue()); 76 | 77 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4}")); 78 | assertEquals(4, value.get().longValue()); 79 | 80 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":0}")); 81 | assertEquals(0, value.get().longValue()); 82 | 83 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":-123}")); 84 | assertEquals(-123, value.get().longValue()); 85 | } 86 | 87 | @Test 88 | public void itSupportsFloatValues() throws PropertyError 89 | { 90 | // given 91 | Thing thing = new Thing("urn:dev:test-123", "My TestThing"); 92 | 93 | Value value = new Value<>(42.0123f, v -> System.out.println("value: " + v)); 94 | thing.addProperty(new Property<>(thing, "p", value, 95 | new JSONObject().put("type", "number"))); 96 | 97 | // when updating property, then 98 | assertEquals(42.0123f, value.get().floatValue(), 0.00001); 99 | 100 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Float.MIN_VALUE+"}")); 101 | assertEquals(Float.MIN_VALUE, value.get().floatValue(), 0.00001); 102 | 103 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Float.MAX_VALUE+"}")); 104 | assertEquals(Float.MAX_VALUE, value.get().floatValue(), 0.00001); 105 | 106 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4.2}")); 107 | assertEquals(4.2f, value.get().floatValue(), 0.00001); 108 | 109 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4}")); 110 | assertEquals(4f, value.get().floatValue(), 0.00001); 111 | 112 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":0}")); 113 | assertEquals(0f, value.get().floatValue(), 0.00001); 114 | 115 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":-123.456}")); 116 | assertEquals(-123.456f, value.get().floatValue(), 0.00001); 117 | } 118 | 119 | @Test 120 | public void itSupportsDoubleValues() throws PropertyError 121 | { 122 | // given 123 | Thing thing = new Thing("urn:dev:test-123", "My TestThing"); 124 | 125 | Value value = new Value<>(42.0123, v -> System.out.println("value: " + v)); 126 | thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "number"))); 127 | 128 | // when updating property, then 129 | assertEquals(42.0123, value.get(), 0.00001); 130 | 131 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Double.MIN_VALUE+"}")); 132 | assertEquals(Double.MIN_VALUE, value.get(), 0.0000000001); 133 | 134 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Double.MAX_VALUE+"}")); 135 | assertEquals(Double.MAX_VALUE, value.get(), 0.0000000001); 136 | 137 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4.2}")); 138 | assertEquals(4.2, value.get(), 0.00001); 139 | 140 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4}")); 141 | assertEquals(4, value.get(), 0.00001); 142 | 143 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":0}")); 144 | assertEquals(0, value.get(), 0.00001); 145 | 146 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":-123.456}")); 147 | assertEquals(-123.456, value.get(), 0.00001); 148 | } 149 | 150 | @Test 151 | public void itSupportsObjectValues() throws PropertyError 152 | { 153 | // given 154 | Thing thing = new Thing("urn:dev:test-123", "My TestThing"); 155 | 156 | Value value = new Value<>(new JSONObject().put("key1", "val1").put("key2", "val2"), 157 | v -> System.out.println("value: " + v)); 158 | thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "object"))); 159 | 160 | // when updating property, then 161 | Map expectedMap = new HashMap<>(); 162 | expectedMap.put("key1", "val1"); 163 | expectedMap.put("key2", "val2"); 164 | assertEquals(expectedMap, value.get().toMap()); 165 | 166 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":{\"key3\":\"val3\"}}")); 167 | assertEquals(Collections.singletonMap("key3", "val3"), value.get().toMap()); 168 | } 169 | 170 | @Test 171 | public void itSupportsArrayValues() throws PropertyError 172 | { 173 | // given 174 | Thing thing = new Thing("urn:dev:test-123", "My TestThing"); 175 | 176 | Value value = new Value<>(new JSONArray("[1,2,3]"), v -> System.out.println("value: " + v)); 177 | thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "array"))); 178 | 179 | // when updating property, then 180 | assertEquals(Arrays.asList(1,2,3), value.get().toList()); 181 | 182 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":[]}")); 183 | assertEquals(Arrays.asList(), value.get().toList()); 184 | } 185 | 186 | @Test 187 | public void itSupportsStringValues() throws PropertyError 188 | { 189 | // given 190 | Thing thing = new Thing("urn:dev:test-123", "My TestThing"); 191 | 192 | Value value = new Value<>("the-initial-string", v -> System.out.println("value: " + v)); 193 | thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "string"))); 194 | 195 | // when updating property, then 196 | assertEquals("the-initial-string", value.get()); 197 | 198 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":\"the-updated-string\"}")); 199 | assertEquals("the-updated-string", value.get()); 200 | } 201 | 202 | @Test 203 | public void itSupportsBooleanValues() throws PropertyError 204 | { 205 | // given 206 | Thing thing = new Thing("urn:dev:test-123", "My TestThing"); 207 | 208 | Value value = new Value<>(false, v -> System.out.println("value: " + v)); 209 | thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "boolean"))); 210 | 211 | // when updating property, then 212 | assertFalse(value.get()); 213 | 214 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":true}")); 215 | assertTrue(value.get()); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/test/java/io/webthings/webthing/ValueTest.java: -------------------------------------------------------------------------------- 1 | package io.webthings.webthing; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNull; 5 | import static org.junit.Assert.assertThrows; 6 | 7 | import java.util.Arrays; 8 | import java.util.Collections; 9 | 10 | import org.json.JSONArray; 11 | import org.json.JSONObject; 12 | import org.junit.Test; 13 | 14 | public class ValueTest { 15 | 16 | @Test 17 | public void itKnowsItsBaseTypeAtRuntime() 18 | { 19 | Value doubleNull = new Value<>(Double.class); 20 | assertEquals(Double.class, doubleNull.getBaseType()); 21 | assertNull(doubleNull.get()); 22 | 23 | Value doubleByValue = new Value<>(42.0123); 24 | assertEquals(Double.class, doubleNull.getBaseType()); 25 | assertEquals(42.0123, doubleByValue.get(), 0.00001); 26 | 27 | Value stringByValue = new Value<>("my-string-value"); 28 | assertEquals(String.class, stringByValue.getBaseType()); 29 | assertEquals("my-string-value", stringByValue.get()); 30 | 31 | Value stringNull = new Value<>(String.class, (String str) -> {}); 32 | assertEquals(String.class, stringByValue.getBaseType()); 33 | assertNull(stringNull.get()); 34 | 35 | Value listByValue = new Value<>(new JSONArray("[1,2,3]"), list -> {}); 36 | assertEquals(JSONArray.class, listByValue.getBaseType()); 37 | assertEquals(Arrays.asList(1, 2, 3), listByValue.get().toList()); 38 | 39 | Value objectExplicit; 40 | objectExplicit = new Value<>(JSONObject.class, new JSONObject().put("key", "value"), obj -> {}); 41 | assertEquals(JSONObject.class, objectExplicit.getBaseType()); 42 | assertEquals(Collections.singletonMap("key", "value"), objectExplicit.get().toMap()); 43 | } 44 | 45 | @Test 46 | public void itsBaseTypeIsRequiredAtConstruction() 47 | { 48 | NullPointerException ex; 49 | ex = assertThrows(NullPointerException.class, () -> new Value(null, true, bool -> {})); 50 | assertEquals("The base type of a value must not be null.", ex.getMessage()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/io/webthings/webthing/example/SingleThingTest.java: -------------------------------------------------------------------------------- 1 | package io.webthings.webthing.example; 2 | 3 | import org.junit.Test; 4 | 5 | public class SingleThingTest { 6 | @Test 7 | public void testTestServer() { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # clone the webthing-tester 4 | git clone https://github.com/WebThingsIO/webthing-tester 5 | pip3 install --user -r webthing-tester/requirements.txt 6 | 7 | # build the jar 8 | mvn clean compile assembly:single test 9 | jar=$(find target -type f -name 'webthing-*-jar-with-dependencies.jar') 10 | 11 | # build and test the single-thing example 12 | java -cp "${jar}" io.webthings.webthing.example.SingleThing & 13 | EXAMPLE_PID=$! 14 | sleep 15 15 | ./webthing-tester/test-client.py 16 | kill -15 $EXAMPLE_PID 17 | wait $EXAMPLE_PID || true 18 | 19 | # build and test the multiple-things example 20 | java -cp "${jar}" io.webthings.webthing.example.MultipleThings & 21 | EXAMPLE_PID=$! 22 | sleep 15 23 | ./webthing-tester/test-client.py --path-prefix "/0" 24 | kill -15 $EXAMPLE_PID 25 | wait $EXAMPLE_PID || true 26 | --------------------------------------------------------------------------------