├── gradle.properties ├── LICENSE ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── kotlin-wot-lmos-protocol ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── protocol │ └── LmosProtocol.kt ├── kotlin-wot-spring-boot-starter ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── spring │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── kotlin │ │ │ └── spring │ │ │ ├── MqttProperties.kt │ │ │ ├── HttpProperties.kt │ │ │ ├── CredentialsProperties.kt │ │ │ └── WoTRuntime.kt │ └── test │ │ ├── kotlin │ │ └── spring │ │ │ ├── TestApplication.kt │ │ │ ├── SpringApplicationTest.kt │ │ │ └── CredentialsPropertiesTest.kt │ │ └── resources │ │ └── application.yaml └── build.gradle.kts ├── .github ├── workflows │ ├── gradle-pr.yml │ ├── reuse-compliance.yml │ ├── gradle.yml │ └── release.yml └── dependabot.yml ├── .devcontainer └── devcontainer.json ├── kotlin-wot-binding-mqtt ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── mqtt │ │ ├── MqttProtocolException.kt │ │ ├── MqttClientConfig.kt │ │ ├── MqttsProtocolClientFactory.kt │ │ └── MqttProtocolClientFactory.kt │ └── test │ ├── resources │ └── logback.xml │ └── kotlin │ └── integration │ └── MqttProtocolClientFactoryTest.kt ├── kotlin-wot-reflection ├── src │ ├── test │ │ └── kotlin │ │ │ └── reflection │ │ │ ├── things │ │ │ ├── SimpleThingInterface.kt │ │ │ └── SimpleThing.kt │ │ │ ├── ConsumedThingBuilderTest.kt │ │ │ ├── MapTypeToSchemaTest.kt │ │ │ ├── BuildObjectSchemaTest.kt │ │ │ └── AddHandlerTest.kt │ └── main │ │ └── kotlin │ │ └── reflection │ │ └── annotations │ │ └── Annotations.kt └── build.gradle.kts ├── kotlin-wot-binding-http ├── src │ ├── test │ │ ├── resources │ │ │ └── logback.xml │ │ └── kotlin │ │ │ └── http │ │ │ ├── HttpProtocolClientFactoryTest.kt │ │ │ ├── HttpsProtocolClientFactoryTest.kt │ │ │ └── HttpProtocolClientTest.kt │ └── main │ │ └── kotlin │ │ └── http │ │ ├── HttpClientConfig.kt │ │ ├── HttpsProtocolClientFactory.kt │ │ ├── HttpProtocolClientFactory.kt │ │ └── routes │ │ ├── ThingsRoute.kt │ │ └── AbstractRoute.kt └── build.gradle.kts ├── kotlin-wot-tool-example ├── src │ └── main │ │ ├── resources │ │ ├── logback.xml │ │ └── application.yaml │ │ └── kotlin │ │ └── example │ │ ├── ToolApplication.kt │ │ └── HtmlTool.kt └── build.gradle.kts ├── kotlin-wot-binding-websocket ├── src │ ├── test │ │ ├── resources │ │ │ └── logback.xml │ │ └── kotlin │ │ │ └── websocket │ │ │ └── WebsocketProtocolClientFactoryTest.kt │ └── main │ │ └── kotlin │ │ └── websocket │ │ ├── HttpClientConfig.kt │ │ ├── SecureWebSocketProtocolClientFactory.kt │ │ └── WebSocketProtocolClientFactory.kt └── build.gradle.kts ├── settings.gradle.kts ├── kotlin-wot ├── src │ ├── main │ │ └── kotlin │ │ │ ├── binding │ │ │ ├── ProtocolServerException.kt │ │ │ ├── ProtocolClientException.kt │ │ │ ├── ProtocolClientNotImplementedException.kt │ │ │ ├── ProtocolServerNotImplementedException.kt │ │ │ ├── ProtocolClientFactory.kt │ │ │ └── ProtocolServer.kt │ │ │ ├── filter │ │ │ ├── ThingFilter.kt │ │ │ ├── DiscoveryMethod.kt │ │ │ └── ThingQuery.kt │ │ │ ├── thing │ │ │ ├── security │ │ │ │ ├── PSKSecurityScheme.kt │ │ │ │ ├── PublicSecurityScheme.kt │ │ │ │ ├── CertSecurityScheme.kt │ │ │ │ ├── NoSecurityScheme.kt │ │ │ │ ├── APIKeySecurityScheme.kt │ │ │ │ ├── DigestSecurityScheme.kt │ │ │ │ ├── BasicSecurityScheme.kt │ │ │ │ ├── SecurityScheme.kt │ │ │ │ ├── OAuth2SecurityScheme.kt │ │ │ │ ├── PoPSecurityScheme.kt │ │ │ │ └── BearerSecurityScheme.kt │ │ │ ├── schema │ │ │ │ ├── Type.kt │ │ │ │ ├── InteractionOutput.kt │ │ │ │ ├── Context.kt │ │ │ │ ├── Exentions.kt │ │ │ │ └── Link.kt │ │ │ ├── OperationsDeserializer.kt │ │ │ ├── TypeSerializer.kt │ │ │ ├── UriTemplate.kt │ │ │ ├── action │ │ │ │ └── ThingAction.kt │ │ │ ├── TypeDeserializer.kt │ │ │ ├── event │ │ │ │ └── ThingEvent.kt │ │ │ ├── form │ │ │ │ ├── Operation.kt │ │ │ │ └── AugmentedForm.kt │ │ │ ├── ContextDeserializer.kt │ │ │ ├── SessionAwareProtocolListenerRegistry.kt │ │ │ ├── ContextSerializer.kt │ │ │ ├── ProtocolHelpers.kt │ │ │ └── ProtocolListenerRegistry.kt │ │ │ ├── content │ │ │ ├── Content.kt │ │ │ ├── JsonCodec.kt │ │ │ ├── LinkFormatCodec.kt │ │ │ └── ContentCodec.kt │ │ │ ├── credentials │ │ │ ├── Credentials.kt │ │ │ └── DefaultCredentialsProvider.kt │ │ │ ├── tracing │ │ │ └── Tracing.kt │ │ │ ├── Utils.kt │ │ │ ├── DefaultWot.kt │ │ │ └── Wot.kt │ └── test │ │ └── kotlin │ │ ├── thing │ │ ├── form │ │ │ ├── OperationTest.kt │ │ │ ├── FormTest.kt │ │ │ └── AugmentedFormTest.kt │ │ ├── TypeTest.kt │ │ ├── UriTemplateTest.kt │ │ ├── action │ │ │ └── ThingActionTest.kt │ │ ├── event │ │ │ └── ThingEventTest.kt │ │ ├── ContextTest.kt │ │ ├── ProtocolHelpersTest.kt │ │ ├── credentials │ │ │ └── DefaultCredentialsProviderTest.kt │ │ ├── SessionAwareProtocolListenerRegistryTest.kt │ │ └── ProtocolListenerRegistryTest.kt │ │ ├── content │ │ ├── JsonCodecText.kt │ │ └── ContentManagerTest.kt │ │ └── WotTest.kt └── build.gradle.kts ├── REUSE.toml ├── .gitignore └── gradlew.bat /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: Robert Winkler 2 | 3 | SPDX-License-Identifier: Apache-2.0 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclipse-thingweb/kotlin-wot/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /kotlin-wot-lmos-protocol/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(platform("io.ktor:ktor-bom:3.0.3")) 3 | implementation("io.ktor:ktor-serialization-jackson") 4 | } -------------------------------------------------------------------------------- /kotlin-wot-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | org.eclipse.thingweb.spring.ServientAutoConfiguration 2 | -------------------------------------------------------------------------------- /.github/workflows/gradle-pr.yml: -------------------------------------------------------------------------------- 1 | name: build-pr 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - 'master' 7 | 8 | jobs: 9 | CI: 10 | uses: eclipse-lmos/.github/.github/workflows/gradle-ci.yml@main 11 | permissions: 12 | contents: read 13 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kotlin-wot", 3 | "image": "mcr.microsoft.com/devcontainers/java:21", 4 | "customizations": { 5 | "vscode": { 6 | "extensions": [ 7 | "vscjava.vscode-java-pack", 8 | "vscjava.vscode-java-test", 9 | ] 10 | } 11 | }, 12 | "postCreateCommand": "./gradlew build" 13 | } 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Deutsche Telekom AG 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | version: 2 6 | updates: 7 | - package-ecosystem: "gradle" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/reuse-compliance.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Deutsche Telekom AG 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | name: REUSE Compliance Check 6 | 7 | on: [push, pull_request] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: REUSE Compliance Check 15 | uses: fsfe/reuse-action@v4 -------------------------------------------------------------------------------- /kotlin-wot-binding-mqtt/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api(project(":kotlin-wot")) 3 | implementation("org.slf4j:slf4j-api:2.0.16") 4 | implementation("com.hivemq:hivemq-mqtt-client:1.3.3") 5 | testImplementation("ch.qos.logback:logback-classic:1.5.12") 6 | testImplementation("app.cash.turbine:turbine:1.2.0") 7 | testImplementation("org.testcontainers:testcontainers:1.20.3") 8 | } 9 | -------------------------------------------------------------------------------- /kotlin-wot-reflection/src/test/kotlin/reflection/things/SimpleThingInterface.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.reflection.things 8 | 9 | import org.eclipse.thingweb.reflection.annotations.Action 10 | 11 | interface SimpleThingInterface { 12 | 13 | @Action() 14 | suspend fun outputAction() : String 15 | } -------------------------------------------------------------------------------- /kotlin-wot-reflection/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api(project(":kotlin-wot")) 3 | implementation("org.slf4j:slf4j-api:2.0.16") 4 | //implementation("ch.qos.logback:logback-classic:1.5.12") 5 | implementation("org.jetbrains.kotlin:kotlin-reflect") 6 | testImplementation("io.mockk:mockk:1.13.13") 7 | testImplementation(project(":kotlin-wot-binding-http")) 8 | testImplementation("com.willowtreeapps.assertk:assertk:0.28.1") 9 | } -------------------------------------------------------------------------------- /kotlin-wot-binding-http/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /kotlin-wot-tool-example/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /kotlin-wot-binding-http/src/main/kotlin/http/HttpClientConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package http 8 | 9 | import org.eclipse.thingweb.security.SecurityScheme 10 | 11 | data class HttpClientConfig( 12 | val port: Int?, 13 | val address: String?, 14 | val allowSelfSigned: Boolean, 15 | val serverKey: String, 16 | val serverCert: String, 17 | val security: SecurityScheme 18 | ) -------------------------------------------------------------------------------- /kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.mqtt 8 | 9 | 10 | internal class MqttProtocolException : Exception { 11 | constructor(message: String) : super(message) 12 | 13 | constructor(message: String, cause: Throwable?) : super(message, cause) 14 | constructor(cause: Throwable?) : super(cause) 15 | } 16 | 17 | -------------------------------------------------------------------------------- /kotlin-wot-spring-boot-starter/src/test/kotlin/spring/TestApplication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.spring 8 | 9 | import org.springframework.boot.autoconfigure.SpringBootApplication 10 | import org.springframework.boot.runApplication 11 | 12 | 13 | fun main(args: Array) { 14 | runApplication(*args) 15 | } 16 | 17 | @SpringBootApplication 18 | class TestApplication { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /kotlin-wot-spring-boot-starter/src/test/resources/application.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # SPDX-FileCopyrightText: Robert Winkler 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | wot: 8 | servient: 9 | security: 10 | credentials: 11 | "[urn:dev:wot:org:eclipse:thingweb:security-example]": 12 | type: bearer 13 | token: test 14 | websocket: 15 | server: 16 | enabled: false 17 | port: 9090 18 | http: 19 | server: 20 | enabled: false 21 | port: 9090 -------------------------------------------------------------------------------- /kotlin-wot-binding-mqtt/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /kotlin-wot-binding-websocket/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /kotlin-wot-binding-websocket/src/main/kotlin/websocket/HttpClientConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.websocket 8 | 9 | import org.eclipse.thingweb.security.SecurityScheme 10 | 11 | data class HttpClientConfig( 12 | val port: Int?, 13 | val address: String?, 14 | val allowSelfSigned: Boolean, 15 | val serverKey: String, 16 | val serverCert: String, 17 | val security: SecurityScheme 18 | ) 19 | -------------------------------------------------------------------------------- /kotlin-wot-tool-example/src/main/kotlin/example/ToolApplication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.example 8 | 9 | import org.springframework.boot.autoconfigure.SpringBootApplication 10 | import org.springframework.boot.runApplication 11 | 12 | 13 | fun main(args: Array) { 14 | runApplication(*args) 15 | } 16 | 17 | @SpringBootApplication 18 | class ThingAgentApplication { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /kotlin-wot-binding-http/src/main/kotlin/http/HttpsProtocolClientFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.http 8 | 9 | /** 10 | * Creates new [HttpProtocolClient] instances that allow consuming Things via HTTPS. 11 | */ 12 | class HttpsProtocolClientFactory() : HttpProtocolClientFactory() { 13 | 14 | override fun toString(): String { 15 | return "HttpsClient" 16 | } 17 | 18 | override val scheme: String 19 | get() = "https" 20 | } 21 | -------------------------------------------------------------------------------- /kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttClientConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.mqtt 8 | 9 | import java.util.* 10 | 11 | data class MqttClientConfig(val host: String, 12 | val port: Int, 13 | val clientId: String = UUID.randomUUID().toString(), 14 | private val username: String? = null, 15 | private val password: String? = null) 16 | 17 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | plugins { 9 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" 10 | } 11 | 12 | rootProject.name = "kotlin-wot" 13 | include("kotlin-wot") 14 | include("kotlin-wot-binding-http") 15 | include("kotlin-wot-binding-mqtt") 16 | include("kotlin-wot-binding-websocket") 17 | include("kotlin-wot-reflection") 18 | include("kotlin-wot-spring-boot-starter") 19 | include("kotlin-wot-tool-example") 20 | include("kotlin-wot-lmos-protocol") -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/binding/ProtocolServerException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package ai.anfc.lmos.wot.binding 8 | 9 | import org.eclipse.thingweb.ServientException 10 | 11 | 12 | /** 13 | * A ProtocolServerException is thrown by [ProtocolServer] implementations when errors occur. 14 | */ 15 | open class ProtocolServerException : ServientException { 16 | constructor(message: String?) : super(message) 17 | constructor(cause: Throwable?) : super(cause) 18 | constructor() : super() 19 | } 20 | -------------------------------------------------------------------------------- /kotlin-wot-binding-websocket/src/main/kotlin/websocket/SecureWebSocketProtocolClientFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.websocket 8 | 9 | /** 10 | * Creates new [WebSocketProtocolClient] instances that allow consuming Things via WSS. 11 | */ 12 | class SecureWebSocketProtocolClientFactory() : WebSocketProtocolClientFactory() { 13 | 14 | override fun toString(): String { 15 | return "SecureWebSocketProtocolClient" 16 | } 17 | 18 | override val scheme: String 19 | get() = "wss" 20 | } 21 | -------------------------------------------------------------------------------- /kotlin-wot-lmos-protocol/src/main/kotlin/protocol/LmosProtocol.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.protocol 8 | 9 | class LMOSThingType { 10 | 11 | companion object { 12 | const val TOOL = "lmos:Tool" 13 | const val AGENT = "lmos:Agent" 14 | } 15 | } 16 | 17 | class LMOSContext { 18 | 19 | companion object { 20 | const val prefix = "lmos" 21 | const val url = "https://eclipse.dev/lmos/protocol/v1" 22 | } 23 | } 24 | 25 | const val LMOS_PROTOCOL_NAME = "lmosprotocol" 26 | 27 | 28 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/binding/ProtocolClientException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package ai.anfc.lmos.wot.binding 8 | 9 | import org.eclipse.thingweb.ServientException 10 | 11 | 12 | /** 13 | * A ProtocolClientException is thrown by [ProtocolClient] implementations when errors occur. 14 | */ 15 | open class ProtocolClientException : ServientException { 16 | constructor(message: String) : super(message) 17 | constructor(cause: Throwable) : super(cause) 18 | constructor(message: String, cause: Exception): super(message, cause) 19 | } 20 | -------------------------------------------------------------------------------- /kotlin-wot-binding-http/src/test/kotlin/http/HttpProtocolClientFactoryTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.http 8 | 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | import kotlin.test.assertIs 12 | 13 | class HttpProtocolClientFactoryTest { 14 | @Test 15 | fun getScheme() { 16 | assertEquals("http", HttpProtocolClientFactory().scheme) 17 | } 18 | 19 | @Test 20 | fun getClient() { 21 | assertIs( HttpProtocolClientFactory().createClient()) 22 | } 23 | } -------------------------------------------------------------------------------- /kotlin-wot-binding-http/src/test/kotlin/http/HttpsProtocolClientFactoryTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.http 8 | 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | import kotlin.test.assertIs 12 | 13 | class HttpsProtocolClientFactoryTest { 14 | 15 | @Test 16 | fun getScheme() { 17 | assertEquals("https", HttpsProtocolClientFactory().scheme) 18 | } 19 | 20 | @Test 21 | fun getClient() { 22 | assertIs( HttpsProtocolClientFactory().createClient()) 23 | } 24 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/binding/ProtocolClientNotImplementedException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package ai.anfc.lmos.wot.binding 8 | 9 | 10 | /** 11 | * This exception is thrown when the a [ProtocolClient] implementation does not support a 12 | * requested functionality. 13 | */ 14 | class ProtocolClientNotImplementedException : ProtocolClientException { 15 | constructor( 16 | clazz: Class<*>, 17 | operation: String 18 | ) : super(clazz.getSimpleName() + " does not implement '" + operation + "'") 19 | 20 | constructor(message: String) : super(message) 21 | } 22 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/binding/ProtocolServerNotImplementedException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package ai.anfc.lmos.wot.binding 8 | 9 | 10 | /** 11 | * This exception is thrown when the a [ProtocolServer] implementation does not support a 12 | * requested functionality. 13 | */ 14 | class ProtocolServerNotImplementedException : ProtocolServerException { 15 | constructor( 16 | clazz: Class<*>, 17 | operation: String 18 | ) : super(clazz.getSimpleName() + " does not implement '" + operation + "'") 19 | 20 | constructor(message: String?) : super(message) 21 | } 22 | -------------------------------------------------------------------------------- /kotlin-wot-binding-mqtt/src/test/kotlin/integration/MqttProtocolClientFactoryTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.mqtt 8 | 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | 12 | class MqttProtocolClientFactoryTest { 13 | 14 | @Test 15 | fun getMqttScheme() { 16 | assertEquals("mqtt", MqttProtocolClientFactory(MqttClientConfig("test", 1, "test")).scheme) 17 | } 18 | @Test 19 | fun getMqttsScheme() { 20 | assertEquals("mqtts", MqttsProtocolClientFactory(MqttClientConfig("test", 2, "test")).scheme) 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Deutsche Telekom AG 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | version = 1 5 | 6 | [[annotations]] 7 | path = [ 8 | ".github/**", 9 | ".gitignore", 10 | "*.md", 11 | ".devcontainer/**", 12 | "*.properties", 13 | "*.kts", 14 | "**/*.kts", 15 | "**/*.xml", 16 | "**/*.conf", 17 | "**/*.yaml", 18 | "**/*.imports" 19 | ] 20 | SPDX-FileCopyrightText = "2024 Robert Winkler" 21 | SPDX-License-Identifier = "Apache-2.0" 22 | 23 | [[annotations]] 24 | path = [ 25 | "gradle/**", 26 | "gradlew", 27 | "gradlew.bat" 28 | ] 29 | SPDX-FileCopyrightText = "Copyright 2015 the original author or authors." 30 | SPDX-License-Identifier = "Apache-2.0" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | .kotlin 7 | 8 | ### IntelliJ IDEA ### 9 | .idea 10 | .idea/modules.xml 11 | .idea/jarRepositories.xml 12 | .idea/compiler.xml 13 | .idea/libraries/ 14 | *.iws 15 | *.iml 16 | *.ipr 17 | out/ 18 | !**/src/main/**/out/ 19 | !**/src/test/**/out/ 20 | 21 | ### Eclipse ### 22 | .apt_generated 23 | .classpath 24 | .factorypath 25 | .project 26 | .settings 27 | .springBeans 28 | .sts4-cache 29 | bin/ 30 | !**/src/main/**/bin/ 31 | !**/src/test/**/bin/ 32 | 33 | ### NetBeans ### 34 | /nbproject/private/ 35 | /nbbuild/ 36 | /dist/ 37 | /nbdist/ 38 | /.nb-gradle/ 39 | 40 | ### VS Code ### 41 | .vscode/ 42 | 43 | ### Mac OS ### 44 | .DS_Store -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/filter/ThingFilter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.filter 8 | 9 | import java.net.URI 10 | 11 | /** 12 | * ThingFilter is used for the discovery process and specifies what things to look for and where to 13 | * look for them. 14 | */ 15 | data class ThingFilter(val url: URI? = null, val query: ThingQuery? = null, var method: DiscoveryMethod = DiscoveryMethod.ANY) { 16 | 17 | override fun toString(): String { 18 | return "ThingFilter{" + 19 | "method=" + method + 20 | ", url=" + url + 21 | ", query=" + query + 22 | '}' 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/filter/DiscoveryMethod.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.filter 8 | 9 | /** 10 | * Defines "Where" to search for things during a discovery process. 11 | */ 12 | enum class DiscoveryMethod { 13 | /** 14 | * Uses the discovery mechanisms provided by all [ProtocolClient] implementations to 15 | * consider all available Things. 16 | */ 17 | ANY, 18 | 19 | /** 20 | * Searches only on the local [Servient]. 21 | */ 22 | LOCAL, 23 | 24 | /** 25 | * Is used together with a URL to search in a specific Thing Directory. 26 | */ 27 | DIRECTORY 28 | // MULTICAST 29 | } 30 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/security/PSKSecurityScheme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.security 8 | 9 | import com.fasterxml.jackson.annotation.JsonInclude 10 | 11 | /** 12 | * Pre-shared key authentication security configuration identified by the term psk (i.e., "scheme": 13 | * "psk").

See also: https://www.w3.org/2019/wot/security#psksecurityscheme 14 | */ 15 | class PSKSecurityScheme(@field:JsonInclude(JsonInclude.Include.NON_EMPTY) val identity: String) : SecurityScheme { 16 | 17 | override fun toString(): String { 18 | return "PSKSecurityScheme{" + 19 | "identity='" + identity + '\'' + 20 | '}' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/security/PublicSecurityScheme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.security 8 | 9 | import com.fasterxml.jackson.annotation.JsonInclude 10 | 11 | /** 12 | * Raw public key asymmetric key security configuration identified by the term public (i.e., 13 | * "scheme": "public").

See also: https://www.w3.org/2019/wot/security#publicsecurityscheme 14 | */ 15 | class PublicSecurityScheme(@field:JsonInclude(JsonInclude.Include.NON_EMPTY) val identity: String) : SecurityScheme { 16 | 17 | override fun toString(): String { 18 | return "PublicSecurityScheme{" + 19 | "identity='" + identity + '\'' + 20 | '}' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /kotlin-wot-binding-websocket/src/test/kotlin/websocket/WebsocketProtocolClientFactoryTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.mqtt 8 | 9 | import org.eclipse.thingweb.binding.websocket.SecureWebSocketProtocolClientFactory 10 | import org.eclipse.thingweb.binding.websocket.WebSocketProtocolClientFactory 11 | import kotlin.test.Test 12 | import kotlin.test.assertEquals 13 | 14 | class WebsocketProtocolClientFactoryTest { 15 | 16 | @Test 17 | fun getWsScheme() { 18 | assertEquals("ws", WebSocketProtocolClientFactory().scheme) 19 | } 20 | @Test 21 | fun getWssScheme() { 22 | assertEquals("wss", SecureWebSocketProtocolClientFactory().scheme) 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /kotlin-wot/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") 3 | api("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.3") 4 | implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.3") 5 | 6 | // Tracing 7 | api(platform("io.opentelemetry:opentelemetry-bom:1.47.0")) 8 | api("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.13.1") 9 | api("io.opentelemetry:opentelemetry-extension-kotlin") 10 | api("io.opentelemetry:opentelemetry-api") 11 | 12 | implementation("org.slf4j:slf4j-api:2.0.16") 13 | testImplementation("net.javacrumbs.json-unit:json-unit-assertj:3.4.1") 14 | testImplementation("io.mockk:mockk:1.13.13") 15 | testImplementation("app.cash.turbine:turbine:1.2.0") 16 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/security/CertSecurityScheme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.security 8 | 9 | import com.fasterxml.jackson.annotation.JsonInclude 10 | 11 | /** 12 | * Certificate-based asymmetric key security configuration conformant with X509V3 identified by the 13 | * term cert (i.e., "scheme": "cert").

See also: https://www.w3.org/2019/wot/security#certsecurityscheme 14 | */ 15 | class CertSecurityScheme(@field:JsonInclude(JsonInclude.Include.NON_EMPTY) val identity: String) : SecurityScheme { 16 | 17 | override fun toString(): String { 18 | return "CertSecurityScheme{" + 19 | "identity='" + identity + '\'' + 20 | '}' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/binding/ProtocolClientFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package ai.anfc.lmos.wot.binding 8 | 9 | 10 | /** 11 | * A ProtocolClientFactory is responsible for creating new [ProtocolClient] instances. There 12 | * is a separate client instance for each [ConsumedThing]. 13 | */ 14 | interface ProtocolClientFactory { 15 | 16 | val scheme: String 17 | 18 | /** 19 | * Is called on servient start. 20 | * 21 | * @return 22 | */ 23 | suspend fun init() 24 | 25 | /** 26 | * Is called on servient shutdown. 27 | * 28 | * @return 29 | */ 30 | suspend fun destroy() 31 | 32 | /** Creates a new [ProtocolClient] */ 33 | fun createClient(): ProtocolClient 34 | } 35 | -------------------------------------------------------------------------------- /kotlin-wot-binding-http/src/main/kotlin/http/HttpProtocolClientFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.http 8 | 9 | import ai.anfc.lmos.wot.binding.ProtocolClient 10 | import ai.anfc.lmos.wot.binding.ProtocolClientFactory 11 | 12 | /** 13 | * Creates new [HttpProtocolClient] instances. 14 | */ 15 | open class HttpProtocolClientFactory() : ProtocolClientFactory { 16 | override fun toString(): String { 17 | return "HttpClient" 18 | } 19 | override val scheme: String 20 | get() = "http" 21 | 22 | override suspend fun init() { 23 | // TODO 24 | } 25 | 26 | override suspend fun destroy() { 27 | // TODO 28 | } 29 | 30 | override fun createClient(): ProtocolClient = HttpProtocolClient() 31 | } 32 | -------------------------------------------------------------------------------- /kotlin-wot-tool-example/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # SPDX-FileCopyrightText: Robert Winkler 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | arc: 8 | ai: 9 | clients: 10 | - id: GPT-4o 11 | model-name: GPT35T-1106 12 | api-key: dummy 13 | client: azure 14 | url: https://gpt4-uk.openai.azure.com 15 | 16 | wot: 17 | servient: 18 | websocket: 19 | server: 20 | enabled: false 21 | host: localhost 22 | port: 9099 23 | http: 24 | server: 25 | enabled: true 26 | host: localhost 27 | port: 9099 28 | mqtt: 29 | server: 30 | enabled: false 31 | host: localhost 32 | port: 54801 33 | clientId: wot-servient 34 | client: 35 | enabled: false 36 | host: localhost 37 | port: 54801 38 | clientId: wot-client -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/schema/Type.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.schema 8 | 9 | import org.eclipse.thingweb.thing.TypeDeserializer 10 | import org.eclipse.thingweb.thing.TypeSerializer 11 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize 12 | import com.fasterxml.jackson.databind.annotation.JsonSerialize 13 | 14 | 15 | @JsonDeserialize(using = TypeDeserializer::class) 16 | @JsonSerialize(using = TypeSerializer::class) 17 | data class Type(val types: MutableSet = HashSet()) { 18 | 19 | constructor(type: String) : this() { 20 | addType(type) 21 | } 22 | 23 | fun addType(type: String): Type { 24 | types.add(type) 25 | return this 26 | } 27 | 28 | val defaultType: String 29 | get() = types.first() 30 | 31 | } 32 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/security/NoSecurityScheme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.security 8 | 9 | 10 | /** 11 | * A security configuration corresponding to identified by the term nosec (i.e., "scheme": "nosec"), 12 | * indicating there is no authentication or other mechanism required to access the resource.

See 13 | * also: https://www.w3.org/2019/wot/security#nosecurityscheme 14 | */ 15 | class NoSecurityScheme : SecurityScheme { 16 | override fun hashCode(): Int { 17 | return 42 18 | } 19 | 20 | override fun equals(o: Any?): Boolean { 21 | return if (this === o) { 22 | true 23 | } else o != null && javaClass == o.javaClass 24 | } 25 | 26 | override fun toString(): String { 27 | return "NoSecurityScheme{}" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /kotlin-wot-binding-http/src/main/kotlin/http/routes/ThingsRoute.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.http.routes 8 | 9 | import org.eclipse.thingweb.content.Content 10 | import org.eclipse.thingweb.content.ContentManager 11 | import org.eclipse.thingweb.thing.ExposedThing 12 | import io.ktor.http.cio.* 13 | import io.ktor.server.routing.* 14 | import io.ktor.util.reflect.* 15 | 16 | /** 17 | * Endpoint for listing all Things from the [io.github.sanecity.wot.Servient]. 18 | */ 19 | class ThingsRoute(private val things: Map) : AbstractRoute() { 20 | @Throws(Exception::class) 21 | fun handle(request: RoutingRequest): Content { 22 | val requestContentType: String = getOrDefaultRequestContentType(request).toString() 23 | return ContentManager.valueToContent(things, requestContentType) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /kotlin-wot-binding-websocket/src/main/kotlin/websocket/WebSocketProtocolClientFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.websocket 8 | 9 | import ai.anfc.lmos.wot.binding.ProtocolClient 10 | import ai.anfc.lmos.wot.binding.ProtocolClientFactory 11 | 12 | /** 13 | * Creates new [WebSocketProtocolClient] instances. 14 | */ 15 | open class WebSocketProtocolClientFactory(private val httpClientConfig: HttpClientConfig? = null) : ProtocolClientFactory { 16 | override fun toString(): String { 17 | return "WebSocketProtocolClient" 18 | } 19 | override val scheme: String 20 | get() = "ws" 21 | 22 | override suspend fun init() { 23 | 24 | } 25 | 26 | override suspend fun destroy() { 27 | 28 | } 29 | 30 | override fun createClient(): ProtocolClient = WebSocketProtocolClient(httpClientConfig) 31 | 32 | } 33 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/OperationsDeserializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing 8 | 9 | import org.eclipse.thingweb.thing.form.Operation 10 | import com.fasterxml.jackson.core.JsonParser 11 | import com.fasterxml.jackson.databind.DeserializationContext 12 | import com.fasterxml.jackson.databind.JsonDeserializer 13 | import com.fasterxml.jackson.databind.JsonNode 14 | 15 | class OperationsDeserializer : JsonDeserializer>() { 16 | override fun deserialize(p: JsonParser, ctxt: DeserializationContext): List { 17 | val node: JsonNode = p.codec.readTree(p) 18 | 19 | return when { 20 | node.isTextual -> listOf(Operation.fromJsonValue(node.asText())) 21 | node.isArray -> node.map { Operation.fromJsonValue(it.asText()) } 22 | else -> emptyList() 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /kotlin-wot/src/test/kotlin/thing/form/OperationTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.form 8 | 9 | import org.eclipse.thingweb.JsonMapper 10 | import com.fasterxml.jackson.core.JsonProcessingException 11 | import java.io.IOException 12 | import kotlin.test.Test 13 | import kotlin.test.assertEquals 14 | 15 | class OperationTest { 16 | @Test 17 | @Throws(JsonProcessingException::class) 18 | fun toJson() { 19 | val op = Operation.READ_PROPERTY 20 | val json = JsonMapper.instance.writeValueAsString(op) 21 | assertEquals("\"readproperty\"", json) 22 | } 23 | 24 | @Test 25 | @Throws(IOException::class) 26 | fun fromJson() { 27 | val json = "\"writeproperty\"" 28 | val op = JsonMapper.instance.readValue(json, Operation::class.java) 29 | assertEquals(Operation.WRITE_PROPERTY, op) 30 | } 31 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/security/APIKeySecurityScheme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.security 8 | 9 | import com.fasterxml.jackson.annotation.JsonInclude 10 | 11 | /** 12 | * API key authentication security configuration identified by the term apikey (i.e., "scheme": 13 | * "apikey"). This is for the case where the access token is opaque and is not using a standard 14 | * token format.

See also: https://www.w3.org/2019/wot/security#apikeysecurityscheme 15 | */ 16 | class APIKeySecurityScheme( 17 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val `in`: String, @field:JsonInclude( 18 | JsonInclude.Include.NON_EMPTY 19 | ) val name: String 20 | ) : SecurityScheme { 21 | 22 | override fun toString(): String { 23 | return "APIKeySecurityScheme{" + 24 | "in='" + `in` + '\'' + 25 | ", name='" + name + '\'' + 26 | '}' 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/TypeSerializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing 8 | 9 | import org.eclipse.thingweb.thing.schema.Type 10 | import com.fasterxml.jackson.core.JsonGenerator 11 | import com.fasterxml.jackson.databind.JsonSerializer 12 | import com.fasterxml.jackson.databind.SerializerProvider 13 | import java.io.IOException 14 | 15 | 16 | class TypeSerializer : JsonSerializer() { 17 | 18 | @Throws(IOException::class) 19 | override fun serialize(type: Type, gen: JsonGenerator, serializers: SerializerProvider) { 20 | val types = type.types 21 | if (types.size == 1) { 22 | gen.writeString(types.iterator().next()) 23 | } else if (types.size > 1) { 24 | gen.writeStartArray() 25 | for (t in types) { 26 | gen.writeString(t) 27 | 28 | } 29 | gen.writeEndArray() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/schema/InteractionOutput.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.schema 8 | 9 | import org.eclipse.thingweb.content.Content 10 | import org.eclipse.thingweb.content.ContentManager 11 | import com.fasterxml.jackson.databind.JsonNode 12 | import kotlinx.coroutines.flow.Flow 13 | 14 | class InteractionOutput( 15 | private val content: Content, 16 | override val schema: DataSchema<*>? 17 | ) : WoTInteractionOutput { 18 | override val data: Flow? 19 | get() = TODO("Not yet implemented") 20 | 21 | override var dataUsed: Boolean = false 22 | 23 | private val lazyValue: JsonNode? by lazy { 24 | schema?.let { ContentManager.contentToValue(content, schema) } 25 | } 26 | override fun arrayBuffer(): ByteArray { 27 | return content.body 28 | } 29 | 30 | override fun value(): JsonNode { 31 | return ContentManager.contentToValue(content, schema) 32 | } 33 | } -------------------------------------------------------------------------------- /kotlin-wot-spring-boot-starter/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | plugins { 4 | kotlin("plugin.spring") version "2.1.20" 5 | id("org.springframework.boot") version "3.1.5" // Use the latest compatible version 6 | id("io.spring.dependency-management") version "1.1.3" 7 | } 8 | 9 | dependencies { 10 | api(project(":kotlin-wot")) 11 | api(project(":kotlin-wot-reflection")) 12 | api("org.springframework.boot:spring-boot-starter") 13 | api("org.springframework.boot:spring-boot-starter-logging") 14 | compileOnly(project(":kotlin-wot-binding-http")) 15 | compileOnly(project(":kotlin-wot-binding-mqtt")) 16 | compileOnly(project(":kotlin-wot-binding-websocket")) 17 | testImplementation("org.springframework.boot:spring-boot-starter-test") 18 | testImplementation(project(":kotlin-wot-binding-http")) 19 | testImplementation(project(":kotlin-wot-binding-websocket")) 20 | } 21 | 22 | tasks.getByName("bootJar") { 23 | enabled = false 24 | } 25 | 26 | tasks.getByName("jar") { 27 | enabled = true 28 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/filter/ThingQuery.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.filter 8 | 9 | import org.eclipse.thingweb.ServientException 10 | import org.eclipse.thingweb.thing.ExposedThing 11 | import org.eclipse.thingweb.thing.schema.WoTExposedThing 12 | 13 | /** 14 | * Is used in the discovery process and filters the things according to certain properties 15 | */ 16 | interface ThingQuery { 17 | 18 | /** 19 | * Applies the filter to the found things and returns only those things that meet the desired 20 | * criteria 21 | * 22 | * @param things 23 | * @return 24 | */ 25 | @Throws(ThingQueryException::class) 26 | fun filter(things: Collection): List 27 | } 28 | 29 | /** 30 | * This exception is thrown when an invalid query is attempted to be used. 31 | */ 32 | class ThingQueryException : ServientException { 33 | constructor(cause: Throwable?) : super(cause) 34 | constructor(message: String?) : super(message) 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /kotlin-wot-spring-boot-starter/src/main/kotlin/spring/MqttProperties.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.spring 8 | 9 | import org.springframework.boot.context.properties.ConfigurationProperties 10 | import org.springframework.validation.annotation.Validated 11 | 12 | @ConfigurationProperties(prefix = "wot.servient.mqtt.server", ignoreUnknownFields = true) 13 | @Validated 14 | data class MqttServerProperties( 15 | var enabled: Boolean = true, 16 | var host: String = "localhost", 17 | var port: Int = 1883, 18 | var clientId : String = "wot-server", 19 | var username: String? = null, 20 | var password: String? = null 21 | ) 22 | 23 | @ConfigurationProperties(prefix = "wot.servient.mqtt.client", ignoreUnknownFields = true) 24 | @Validated 25 | data class MqttClientProperties( 26 | var enabled: Boolean = true, 27 | var host: String = "localhost", 28 | var port: Int = 1883, 29 | var clientId : String = "wot-client", 30 | var username: String? = null, 31 | var password: String? = null 32 | ) -------------------------------------------------------------------------------- /kotlin-wot-binding-http/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(platform("io.ktor:ktor-bom:3.0.3")) 3 | api(project(":kotlin-wot")) 4 | implementation("io.ktor:ktor-server-core") 5 | implementation("io.ktor:ktor-server-netty") 6 | implementation("io.ktor:ktor-client-core") 7 | implementation("io.ktor:ktor-server-status-pages") 8 | implementation("io.ktor:ktor-client-cio") 9 | implementation("io.ktor:ktor-client-auth") 10 | implementation("io.ktor:ktor-client-logging") 11 | implementation("io.ktor:ktor-server-content-negotiation") 12 | implementation("io.ktor:ktor-client-content-negotiation") 13 | implementation("io.ktor:ktor-serialization-jackson") 14 | implementation("io.ktor:ktor-server-metrics-micrometer") 15 | implementation("io.ktor:ktor-server-auto-head-response") 16 | 17 | implementation("io.opentelemetry.instrumentation:opentelemetry-ktor-3.0:2.13.1-alpha") 18 | 19 | testImplementation("io.ktor:ktor-server-test-host") 20 | testImplementation("ch.qos.logback:logback-classic:1.5.12") 21 | testImplementation("com.marcinziolo:kotlin-wiremock:2.1.1") 22 | } 23 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/security/DigestSecurityScheme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.security 8 | 9 | import com.fasterxml.jackson.annotation.JsonInclude 10 | 11 | /** 12 | * Digest authentication security configuration identified by the term digest (i.e., "scheme": 13 | * "digest"). This scheme is similar to basic authentication but with added features to avoid 14 | * man-in-the-middle attacks.

See also: https://www.w3.org/2019/wot/security#digestsecurityscheme 15 | */ 16 | class DigestSecurityScheme( 17 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val `in`: String, @field:JsonInclude( 18 | JsonInclude.Include.NON_EMPTY 19 | ) val name: String, @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val qop: String 20 | ) : SecurityScheme { 21 | 22 | override fun toString(): String { 23 | return "DigestSecurityScheme{" + 24 | "in='" + `in` + '\'' + 25 | ", name='" + name + '\'' + 26 | ", qop='" + qop + '\'' + 27 | '}' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Deutsche Telekom AG 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | # This workflow uses actions that are not certified by GitHub. 6 | # They are provided by a third-party and are governed by 7 | # separate terms of service, privacy policy, and support 8 | # documentation. 9 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 10 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle 11 | 12 | name: build-publish 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | 18 | jobs: 19 | build-publish: 20 | uses: eclipse-lmos/.github/.github/workflows/gradle-ci-main.yml@main 21 | permissions: 22 | contents: write 23 | packages: write 24 | secrets: 25 | oss-username: ${{ secrets.OSSRH_USERNAME }} 26 | oss-password: ${{ secrets.OSSRH_PASSWORD }} 27 | signing-key-id: ${{ secrets.GPG_SUBKEY_ID }} 28 | signing-key: ${{ secrets.GPG_PRIVATE_KEY }} 29 | signing-key-password: ${{ secrets.GPG_PASSPHRASE }} 30 | -------------------------------------------------------------------------------- /kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttsProtocolClientFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.mqtt 8 | 9 | import ai.anfc.lmos.wot.binding.ProtocolClient 10 | import ai.anfc.lmos.wot.binding.ProtocolClientFactory 11 | import com.hivemq.client.mqtt.mqtt5.Mqtt5Client 12 | 13 | open class MqttsProtocolClientFactory(private val mqttClientConfig: MqttClientConfig) : ProtocolClientFactory { 14 | override fun toString(): String { 15 | return "MqttClient" 16 | } 17 | override val scheme: String 18 | get() = "mqtts" 19 | 20 | override suspend fun init() { 21 | } 22 | 23 | override suspend fun destroy() { 24 | } 25 | 26 | override fun createClient(): ProtocolClient = 27 | MqttProtocolClient(Mqtt5Client.builder() 28 | .identifier(mqttClientConfig.clientId) 29 | .serverHost(mqttClientConfig.host) 30 | .serverPort(mqttClientConfig.port) 31 | .sslWithDefaultConfig() 32 | .automaticReconnect().applyAutomaticReconnect() 33 | .build().toAsync(), true) 34 | } 35 | -------------------------------------------------------------------------------- /kotlin-wot-binding-websocket/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(platform("io.ktor:ktor-bom:3.0.3")) 3 | 4 | api(project(":kotlin-wot")) 5 | api(project(":kotlin-wot-lmos-protocol")) 6 | implementation("io.ktor:ktor-server-netty") 7 | implementation("io.ktor:ktor-server-websockets") 8 | implementation("io.ktor:ktor-client-websocket:1.1.4") 9 | implementation("io.ktor:ktor-server-content-negotiation") 10 | 11 | // Tracing 12 | implementation("io.opentelemetry.instrumentation:opentelemetry-ktor-3.0:2.13.1-alpha") 13 | 14 | implementation("io.ktor:ktor-client-cio") 15 | implementation("io.ktor:ktor-client-auth") 16 | implementation("io.ktor:ktor-client-logging") 17 | implementation("io.ktor:ktor-server-call-logging") 18 | implementation("io.ktor:ktor-serialization-jackson") 19 | implementation("io.ktor:ktor-server-metrics-micrometer") 20 | testImplementation("io.ktor:ktor-server-test-host") 21 | testImplementation(project(":kotlin-wot-binding-http")) 22 | testImplementation("ch.qos.logback:logback-classic:1.5.12") 23 | testImplementation("com.marcinziolo:kotlin-wiremock:2.1.1") 24 | testImplementation("io.ktor:ktor-server-test-host-jvm:3.0.0") 25 | } 26 | -------------------------------------------------------------------------------- /kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolClientFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.mqtt 8 | 9 | import ai.anfc.lmos.wot.binding.ProtocolClient 10 | import ai.anfc.lmos.wot.binding.ProtocolClientFactory 11 | import com.hivemq.client.mqtt.mqtt5.Mqtt5Client 12 | 13 | open class MqttProtocolClientFactory(private val mqttClientConfig: MqttClientConfig) : ProtocolClientFactory { 14 | 15 | override fun toString(): String { 16 | return "MqttClient" 17 | } 18 | override val scheme: String 19 | get() = "mqtt" 20 | 21 | override suspend fun init() { 22 | 23 | } 24 | 25 | override suspend fun destroy() { 26 | 27 | } 28 | 29 | override fun createClient(): ProtocolClient = 30 | MqttProtocolClient( 31 | Mqtt5Client.builder() 32 | .identifier(mqttClientConfig.clientId) 33 | .serverHost(mqttClientConfig.host) 34 | .serverPort(mqttClientConfig.port) 35 | //.automaticReconnect() 36 | //.applyAutomaticReconnect() 37 | .build() 38 | .toAsync()) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /kotlin-wot-spring-boot-starter/src/main/kotlin/spring/HttpProperties.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.spring 8 | 9 | import org.springframework.boot.context.properties.ConfigurationProperties 10 | import org.springframework.validation.annotation.Validated 11 | 12 | open class ServerProperties( 13 | var enabled: Boolean = true, 14 | var host: String = "0.0.0.0", 15 | var port: Int = 8080, 16 | var baseUrls: List 17 | ) 18 | 19 | 20 | @ConfigurationProperties(prefix = "wot.servient.http.server", ignoreUnknownFields = true) 21 | @Validated 22 | class HttpServerProperties( 23 | enabled: Boolean = true, 24 | host: String = "0.0.0.0", 25 | port: Int = 8080, 26 | baseUrls: List = listOf("http://localhost:$port") 27 | ) : ServerProperties(enabled, host, port, baseUrls) 28 | 29 | @ConfigurationProperties(prefix = "wot.servient.websocket.server", ignoreUnknownFields = true) 30 | @Validated 31 | class WebsocketProperties( 32 | enabled: Boolean = true, 33 | host: String = "0.0.0.0", 34 | port: Int = 8080, 35 | baseUrls: List = listOf("ws://localhost:$port") 36 | ) : ServerProperties(enabled, host, port, baseUrls) -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/content/Content.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.content 8 | 9 | /** 10 | * Represents any serialized content. Enables the transfer of arbitrary data structures. 11 | */ 12 | data class Content(val type: String = ContentManager.DEFAULT_MEDIA_TYPE, val body: ByteArray = ByteArray(0)) { 13 | 14 | companion object { 15 | val EMPTY_CONTENT = Content(ContentManager.DEFAULT_MEDIA_TYPE, ByteArray(0)) 16 | } 17 | 18 | override fun equals(other: Any?): Boolean { 19 | if (this === other) return true 20 | if (javaClass != other?.javaClass) return false 21 | 22 | other as Content 23 | 24 | if (type != other.type) return false 25 | if (!body.contentEquals(other.body)) return false 26 | 27 | return true 28 | } 29 | 30 | override fun hashCode(): Int { 31 | var result = type.hashCode() 32 | result = 31 * result + body.contentHashCode() 33 | return result 34 | } 35 | } 36 | 37 | fun String.toJsonContent(): Content{ 38 | val jsonContent = """"$this"""" 39 | return Content(ContentManager.DEFAULT_MEDIA_TYPE, jsonContent.toByteArray()) 40 | } 41 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/binding/ProtocolServer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package ai.anfc.lmos.wot.binding 8 | 9 | import org.eclipse.thingweb.Servient 10 | import org.eclipse.thingweb.thing.ExposedThing 11 | 12 | /** 13 | * A ProtocolServer defines how to expose Thing for interaction via a specific protocol (e.g. HTTP, 14 | * MQTT, etc.). 15 | */ 16 | interface ProtocolServer { 17 | 18 | /** 19 | * Starts the server (e.g. HTTP server) and makes it ready for requests to the exposed things. 20 | * 21 | * @param servient 22 | * @return 23 | */ 24 | suspend fun start(servient: Servient) 25 | 26 | /** 27 | * Stops the server (e.g. HTTP server) and ends the exposure of the Things 28 | * 29 | * @return 30 | */ 31 | suspend fun stop() 32 | 33 | /** 34 | * Exposes `thing` and allows interaction with it. 35 | * 36 | * @param thing 37 | * @return 38 | */ 39 | suspend fun expose(thing: ExposedThing) 40 | 41 | /** 42 | * Stops the exposure of `thing` and allows no further interaction with the thing. 43 | * 44 | * @param thing 45 | * @return 46 | */ 47 | suspend fun destroy(thing: ExposedThing) 48 | 49 | } 50 | -------------------------------------------------------------------------------- /kotlin-wot-reflection/src/test/kotlin/reflection/ConsumedThingBuilderTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package reflection 8 | 9 | import org.eclipse.thingweb.Servient 10 | import org.eclipse.thingweb.Wot 11 | import org.eclipse.thingweb.reflection.ExposedThingBuilder 12 | import org.eclipse.thingweb.reflection.things.ComplexThing 13 | import org.eclipse.thingweb.thing.schema.WoTExposedThing 14 | import org.eclipse.thingweb.thing.schema.WoTThingDescription 15 | import kotlin.test.BeforeTest 16 | 17 | class ConsumedThingBuilderTest { 18 | 19 | lateinit var servient: Servient 20 | lateinit var wot: Wot 21 | lateinit var complexThing: ComplexThing 22 | lateinit var exposedThing: WoTExposedThing 23 | lateinit var thingDescription: WoTThingDescription 24 | 25 | @BeforeTest 26 | fun setUp() { 27 | // Set up the servient and WoT instance 28 | servient = Servient() 29 | wot = Wot.create(servient) 30 | 31 | // Create an instance of ComplexThing 32 | complexThing = ComplexThing() 33 | 34 | // Generate ThingDescription from the class 35 | exposedThing = ExposedThingBuilder.createExposedThing(wot, complexThing, ComplexThing::class) as WoTExposedThing 36 | } 37 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/security/BasicSecurityScheme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.security 8 | 9 | import com.fasterxml.jackson.annotation.JsonInclude 10 | import java.util.* 11 | 12 | /** 13 | * Basic authentication security configuration identified by the term basic (i.e., "scheme": 14 | * "basic"), using an unencrypted username and password. This scheme should be used with some other 15 | * security mechanism providing confidentiality, for example, TLS.

See also: 16 | * https://www.w3.org/2019/wot/security#basicsecurityscheme 17 | */ 18 | class BasicSecurityScheme @JvmOverloads constructor(@field:JsonInclude(JsonInclude.Include.NON_EMPTY) var `in`: String? = null) : 19 | SecurityScheme { 20 | 21 | override fun hashCode(): Int { 22 | return Objects.hash(`in`) 23 | } 24 | 25 | override fun equals(o: Any?): Boolean { 26 | if (this === o) { 27 | return true 28 | } 29 | if (o !is BasicSecurityScheme) { 30 | return false 31 | } 32 | return `in` == o.`in` 33 | } 34 | 35 | override fun toString(): String { 36 | return "BasicSecurityScheme{" + 37 | "in='" + `in` + '\'' + 38 | '}' 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /kotlin-wot/src/test/kotlin/thing/form/FormTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.form 8 | 9 | import org.eclipse.thingweb.JsonMapper 10 | import net.javacrumbs.jsonunit.assertj.JsonAssertions 11 | import net.javacrumbs.jsonunit.core.Option 12 | import kotlin.test.Test 13 | import kotlin.test.assertEquals 14 | 15 | class FormTest { 16 | @Test 17 | fun testForm() { 18 | val form = Form( 19 | href = "test:/foo", 20 | op = listOf( Operation.OBSERVE_PROPERTY), 21 | subprotocol = "longpolling", 22 | contentType = "application/json" 23 | ) 24 | assertEquals("test:/foo", form.href) 25 | assertEquals(listOf(Operation.OBSERVE_PROPERTY), form.op) 26 | assertEquals("longpolling", form.subprotocol) 27 | assertEquals("application/json", form.contentType) 28 | 29 | val json = JsonMapper.instance.writeValueAsString(form) 30 | 31 | JsonAssertions.assertThatJson(json) 32 | .`when`(Option.IGNORING_ARRAY_ORDER) 33 | .isEqualTo( 34 | """{"href":"test:/foo", 35 | "op":["observeproperty"], 36 | "subprotocol":"longpolling", 37 | "contentType":"application/json"} 38 | """ 39 | ) 40 | } 41 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/schema/Context.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.schema 8 | 9 | import org.eclipse.thingweb.thing.ContextDeserializer 10 | import org.eclipse.thingweb.thing.ContextSerializer 11 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize 12 | import com.fasterxml.jackson.databind.annotation.JsonSerialize 13 | 14 | /** 15 | * Represents a JSON-LD context. 16 | */ 17 | @JsonDeserialize(using = ContextDeserializer::class) 18 | @JsonSerialize(using = ContextSerializer::class) 19 | data class Context(val defaultUrls: MutableList = mutableListOf(), private val prefixeUrls: MutableMap = HashMap()) { 20 | 21 | constructor(url: String) : this() { 22 | addContext(url) 23 | } 24 | 25 | constructor(prefix: String, url: String) : this() { 26 | addContext(prefix, url) 27 | } 28 | 29 | fun addContext(url: String): Context { 30 | defaultUrls.add(url) 31 | return this 32 | } 33 | 34 | fun addContext(prefix: String, url: String): Context { 35 | prefixeUrls[prefix] = url 36 | return this 37 | } 38 | 39 | val prefixedUrls: Map 40 | get() = prefixeUrls.entries 41 | .filter { (key, _) -> key != null } 42 | .associate { (key, value) -> key!! to value } 43 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/credentials/Credentials.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.credentials 8 | 9 | import org.eclipse.thingweb.thing.schema.WoTForm 10 | import com.fasterxml.jackson.annotation.JsonSubTypes 11 | import com.fasterxml.jackson.annotation.JsonTypeInfo 12 | import java.util.* 13 | 14 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") 15 | @JsonSubTypes( 16 | JsonSubTypes.Type(value = BasicCredentials::class, name = "basic"), 17 | JsonSubTypes.Type(value = BearerCredentials::class, name = "bearer"), 18 | JsonSubTypes.Type(value = ApiKeyCredentials::class, name = "apikey") 19 | ) 20 | interface Credentials { 21 | } 22 | 23 | fun interface CredentialsProvider { 24 | fun getCredentials(form : WoTForm): Credentials? 25 | } 26 | 27 | data class BearerCredentials( 28 | val token: String 29 | ) : Credentials{ 30 | override fun toString(): String { 31 | return "Bearer $token" 32 | } 33 | } 34 | 35 | data class BasicCredentials( 36 | val username: String, 37 | val password: String 38 | ) : Credentials { 39 | override fun toString(): String { 40 | val basicAuth = Base64.getEncoder().encodeToString("$username:$password".toByteArray()) 41 | return "Basic $basicAuth" 42 | } 43 | } 44 | 45 | data class ApiKeyCredentials( 46 | val apiKey: String 47 | ) : Credentials -------------------------------------------------------------------------------- /kotlin-wot-tool-example/src/main/kotlin/example/HtmlTool.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.example 8 | 9 | 10 | import io.opentelemetry.api.trace.Span 11 | import io.opentelemetry.instrumentation.annotations.WithSpan 12 | import org.eclipse.thingweb.protocol.LMOSContext 13 | import org.eclipse.thingweb.protocol.LMOSThingType 14 | import org.eclipse.thingweb.reflection.annotations.* 15 | import org.jsoup.Jsoup 16 | import org.springframework.stereotype.Component 17 | 18 | 19 | @Thing(id= "scraper", title="Tool", 20 | description= "An HTML scraper.", type = LMOSThingType.TOOL) 21 | @VersionInfo(instance = "1.0.0") 22 | @Context(prefix = LMOSContext.prefix, url = LMOSContext.url) 23 | @Component 24 | class HtmlTool() { 25 | 26 | @Action(title = "Fetch Content", description = "Fetches the content from the specified URL.") 27 | @ActionInput(title = "url", description = "The URL to fetch content from.") 28 | @ActionOutput(title = "content", description = "The content fetched from the URL.") 29 | @WithSpan 30 | suspend fun fetchContent(url: String): String { 31 | Span.current().setAttribute("lmos.agent.scraper.input.url", url) 32 | return try { 33 | val document = Jsoup.connect(url).get() 34 | document.outerHtml() 35 | } catch (e: Exception) { 36 | "Error fetching content" 37 | } 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/security/SecurityScheme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.security 8 | 9 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 10 | import com.fasterxml.jackson.annotation.JsonSubTypes 11 | import com.fasterxml.jackson.annotation.JsonTypeInfo 12 | 13 | /** 14 | * Describes properties of a security mechanism (e.g. password authentication).

See also: 15 | * https://www.w3.org/TR/wot-thing-description/#security-serialization-json 16 | */ 17 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "scheme") 18 | @JsonSubTypes( 19 | JsonSubTypes.Type(value = APIKeySecurityScheme::class, name = "apikey"), 20 | JsonSubTypes.Type(value = BasicSecurityScheme::class, name = "basic"), 21 | JsonSubTypes.Type(value = BearerSecurityScheme::class, name = "bearer"), 22 | JsonSubTypes.Type(value = CertSecurityScheme::class, name = "cert"), 23 | JsonSubTypes.Type(value = DigestSecurityScheme::class, name = "digest"), 24 | JsonSubTypes.Type(value = NoSecurityScheme::class, name = "nosec"), 25 | JsonSubTypes.Type(value = OAuth2SecurityScheme::class, name = "oauth2"), 26 | JsonSubTypes.Type(value = PoPSecurityScheme::class, name = "pop"), 27 | JsonSubTypes.Type(value = PSKSecurityScheme::class, name = "psk"), 28 | JsonSubTypes.Type(value = PublicSecurityScheme::class, name = "public") 29 | ) 30 | @JsonIgnoreProperties(ignoreUnknown = true) 31 | interface SecurityScheme 32 | -------------------------------------------------------------------------------- /kotlin-wot/src/test/kotlin/thing/form/AugmentedFormTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.form 8 | 9 | import AugmentedForm 10 | import org.eclipse.thingweb.security.BasicSecurityScheme 11 | import org.eclipse.thingweb.thing.schema.WoTForm 12 | import org.eclipse.thingweb.thing.schema.WoTThingDescription 13 | import io.mockk.every 14 | import io.mockk.mockk 15 | import kotlin.test.Test 16 | import kotlin.test.assertEquals 17 | 18 | class AugmentedFormTest { 19 | 20 | private val mockForm = mockk { 21 | every { href } returns "/resource/{id}" 22 | every { security } returns listOf("basic") 23 | } 24 | 25 | private val mockThingDescription = mockk { 26 | every { base } returns "https://example.com/api/" 27 | every { securityDefinitions } returns mutableMapOf( 28 | "basic" to BasicSecurityScheme("BasicAuth") 29 | ) 30 | } 31 | 32 | @Test 33 | fun `href should resolve correctly with base`() { 34 | val augmentedForm = AugmentedForm(mockForm, mockThingDescription) 35 | 36 | val expectedHref = "https://example.com/api/resource/{id}" 37 | assertEquals(expectedHref, augmentedForm.href) 38 | } 39 | 40 | @Test 41 | fun `securityDefinitions should be mapped correctly`() { 42 | val augmentedForm = AugmentedForm(mockForm, mockThingDescription) 43 | 44 | assertEquals(1, augmentedForm.securityDefinitions.size) 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/UriTemplate.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing 8 | 9 | import java.util.regex.Pattern 10 | 11 | class UriTemplate(private val template: String) { 12 | 13 | companion object { 14 | // A method to create a UriTemplate from a template string 15 | fun fromTemplate(template: String): UriTemplate { 16 | return UriTemplate(template) 17 | } 18 | } 19 | 20 | // Expands the URI template with the provided uriVariables map 21 | fun expand(uriVariables: Map): String { 22 | var expandedUri = template 23 | 24 | // Iterate over all uriVariables and replace placeholders in the template 25 | uriVariables.forEach { (key, value) -> 26 | // Replace the placeholder {key} with the value from the uriVariables map 27 | expandedUri = expandedUri.replace("{$key}", value) 28 | } 29 | 30 | // Handle any remaining unresolved placeholders, for example, if a placeholder is missing a value. 31 | // For this example, let's throw an exception if we have unresolved placeholders 32 | val remainingPlaceholders = Pattern.compile("\\{([a-zA-Z0-9_]+)\\}") 33 | .matcher(expandedUri) 34 | 35 | if (remainingPlaceholders.find()) { 36 | throw IllegalArgumentException("Template contains unresolved placeholders: ${remainingPlaceholders.group()}") 37 | } 38 | 39 | return expandedUri 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /kotlin-wot/src/test/kotlin/thing/TypeTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing 8 | 9 | import org.eclipse.thingweb.JsonMapper 10 | import org.eclipse.thingweb.thing.schema.Type 11 | import com.fasterxml.jackson.core.JsonProcessingException 12 | import net.javacrumbs.jsonunit.assertj.JsonAssertions 13 | import net.javacrumbs.jsonunit.core.Option 14 | import java.io.IOException 15 | import kotlin.test.Test 16 | import kotlin.test.assertEquals 17 | 18 | internal class TypeTest { 19 | @Test 20 | @Throws(IOException::class) 21 | fun fromJson() { 22 | // single value 23 | assertEquals( 24 | Type("Thing"), 25 | JsonMapper.instance.readValue("\"Thing\"", Type::class.java) 26 | ) 27 | 28 | // array 29 | assertEquals( 30 | Type("Thing").addType("saref:LightSwitch"), 31 | JsonMapper.instance.readValue("[\"Thing\",\"saref:LightSwitch\"]", Type::class.java) 32 | ) 33 | } 34 | 35 | @Test 36 | @Throws(JsonProcessingException::class) 37 | fun toJson() { 38 | // single value 39 | assertEquals( 40 | "\"Thing\"", 41 | JsonMapper.instance.writeValueAsString(Type("Thing")) 42 | ) 43 | 44 | // multi type array 45 | JsonAssertions.assertThatJson(JsonMapper.instance.writeValueAsString(Type("Thing").addType("saref:LightSwitch"))) 46 | .`when`(Option.IGNORING_ARRAY_ORDER) 47 | .isArray() 48 | .contains("Thing", "saref:LightSwitch") 49 | } 50 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/security/OAuth2SecurityScheme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.security 8 | 9 | import com.fasterxml.jackson.annotation.JsonInclude 10 | 11 | /** 12 | * OAuth2 authentication security configuration for systems conformant with !RFC6749 and !RFC8252, 13 | * identified by the term oauth2 (i.e., "scheme": "oauth2"). For the implicit flow authorization 14 | * MUST be included. For the password and client flows token MUST be included. For the code flow 15 | * both authorization and token MUST be included. If no scopes are defined in the SecurityScheme 16 | * then they are considered to be empty.

See also: https://www.w3.org/2019/wot/security#oauth2securityscheme 17 | */ 18 | class OAuth2SecurityScheme( 19 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val authorization: String?, 20 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val flow: String, 21 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val token: String?, 22 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val refresh: String?, 23 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val scopes: List? 24 | ) : SecurityScheme { 25 | 26 | override fun toString(): String { 27 | return "OAuth2SecurityScheme{" + 28 | "authorization='" + authorization + '\'' + 29 | ", flow='" + flow + '\'' + 30 | ", token='" + token + '\'' + 31 | ", refresh='" + refresh + '\'' + 32 | ", scopes=" + scopes + 33 | '}' 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/security/PoPSecurityScheme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.security 8 | 9 | import com.fasterxml.jackson.annotation.JsonInclude 10 | 11 | /** 12 | * Proof-of-possession (PoP) token authentication security configuration identified by the term pop 13 | * (i.e., "scheme": "pop"). Here jwt indicates conformance with !RFC7519, jws indicates conformance 14 | * with !RFC7797, cwt indicates conformance with !RFC8392, and jwe indicates conformance with 15 | * RFC7516, with values for alg interpreted consistently with those standards. Other formats and 16 | * algorithms for PoP tokens MAY be specified in vocabulary extensions.

See also: 17 | * https://www.w3.org/2019/wot/security#popsecurityscheme 18 | */ 19 | class PoPSecurityScheme( 20 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val `in`: String, 21 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val name: String, 22 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val format: String, 23 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val authorization: String, 24 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val alg: String 25 | ) : SecurityScheme { 26 | 27 | override fun toString(): String { 28 | return "PoPSecurityScheme{" + 29 | "in='" + `in` + '\'' + 30 | ", name='" + name + '\'' + 31 | ", format='" + format + '\'' + 32 | ", authorization='" + authorization + '\'' + 33 | ", alg='" + alg + '\'' + 34 | '}' 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /kotlin-wot-binding-http/src/main/kotlin/http/routes/AbstractRoute.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.http.routes 8 | 9 | import org.eclipse.thingweb.binding.http.routes.AbstractRoute 10 | import org.eclipse.thingweb.content.ContentManager 11 | import org.eclipse.thingweb.content.ContentManager.isSupportedMediaType 12 | import io.ktor.http.* 13 | import io.ktor.server.request.* 14 | import io.ktor.server.routing.* 15 | import org.slf4j.LoggerFactory 16 | 17 | /** 18 | * Abstract route for exposing Things. Inherited from all other routes. 19 | */ 20 | abstract class AbstractRoute { 21 | 22 | fun getOrDefaultRequestContentType(request: RoutingRequest): ContentType { 23 | val contentType = request.contentType() 24 | // Check if the content type is of type `Any` and return the default 25 | return if (contentType == ContentType.Any) { 26 | ContentType.Application.Json 27 | } else { 28 | contentType 29 | } 30 | } 31 | 32 | fun unsupportedMediaTypeResponse(response: RoutingResponse, requestContentType: String?): String? { 33 | return if (!isSupportedMediaType(requestContentType)) { 34 | response.status(HttpStatusCode.UnsupportedMediaType) 35 | "Unsupported Media Type (supported: " + java.lang.String.join( 36 | ", ", 37 | ContentManager.offeredMediaTypes 38 | ) + ")" 39 | } else { 40 | null 41 | } 42 | } 43 | 44 | companion object { 45 | val log = LoggerFactory.getLogger(AbstractRoute::class.java) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/tracing/Tracing.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.tracing 8 | 9 | import io.opentelemetry.api.GlobalOpenTelemetry 10 | import io.opentelemetry.api.trace.Span 11 | import io.opentelemetry.api.trace.SpanBuilder 12 | import io.opentelemetry.api.trace.StatusCode 13 | import io.opentelemetry.api.trace.Tracer 14 | import io.opentelemetry.extension.kotlin.asContextElement 15 | import kotlinx.coroutines.withContext 16 | import kotlin.coroutines.CoroutineContext 17 | import kotlin.coroutines.EmptyCoroutineContext 18 | 19 | 20 | val tracer = GlobalOpenTelemetry.getTracer("kotlin-wot") 21 | 22 | suspend fun Tracer.startSpan( 23 | spanName: String, 24 | parameters: (SpanBuilder.() -> Unit)? = null, 25 | coroutineContext: CoroutineContext = EmptyCoroutineContext, 26 | block: suspend (span: Span) -> T 27 | ): T { 28 | val span: Span = this.spanBuilder(spanName).run { 29 | if (parameters != null) parameters() 30 | startSpan() 31 | } 32 | 33 | return withContext(coroutineContext + span.asContextElement()) { 34 | try { 35 | block(span) 36 | } catch (throwable: Throwable) { 37 | span.setStatus(StatusCode.ERROR) 38 | span.recordException(throwable) 39 | throw throwable 40 | } finally { 41 | span.end() 42 | } 43 | } 44 | } 45 | 46 | suspend fun withSpan( 47 | spanName: String, 48 | parameters: (SpanBuilder.() -> Unit)? = null, 49 | coroutineContext: CoroutineContext = EmptyCoroutineContext, 50 | block: suspend (span: Span) -> T 51 | ): T = tracer.startSpan(spanName, parameters, coroutineContext, block) -------------------------------------------------------------------------------- /kotlin-wot-spring-boot-starter/src/test/kotlin/spring/SpringApplicationTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.spring 8 | 9 | import org.eclipse.thingweb.Servient 10 | import org.eclipse.thingweb.Wot 11 | import org.eclipse.thingweb.credentials.BearerCredentials 12 | import org.springframework.beans.factory.annotation.Autowired 13 | import org.springframework.boot.test.context.SpringBootTest 14 | import org.springframework.core.env.Environment 15 | import kotlin.test.Test 16 | import kotlin.test.assertContains 17 | import kotlin.test.assertEquals 18 | import kotlin.test.assertNotNull 19 | 20 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) 21 | class SpringApplicationTest { 22 | 23 | @Autowired 24 | private lateinit var credentialsProperties: CredentialsProperties 25 | 26 | @Autowired 27 | private lateinit var httpServerProperties: HttpServerProperties 28 | 29 | @Autowired 30 | private lateinit var wot: Wot 31 | 32 | @Autowired 33 | private lateinit var servient: Servient 34 | 35 | @Autowired 36 | lateinit var env: Environment 37 | 38 | @Test 39 | fun `should load http server properties from application properties`() { 40 | assertEquals(false, httpServerProperties.enabled) 41 | assertEquals(9090, httpServerProperties.port) 42 | } 43 | 44 | @Test 45 | fun `should initiate wot object`() { 46 | assertNotNull(wot) 47 | } 48 | 49 | @Test 50 | fun `should initiate servient object`() { 51 | assertContains(servient.getClientSchemes(), "https") 52 | assertEquals(BearerCredentials("test"), servient.credentialStore["urn:dev:wot:org:eclipse:thingweb:security-example"]) 53 | } 54 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/action/ThingAction.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.action 8 | 9 | import org.eclipse.thingweb.thing.schema.Type 10 | import org.eclipse.thingweb.thing.form.Form 11 | import org.eclipse.thingweb.thing.schema.ActionAffordance 12 | import org.eclipse.thingweb.thing.schema.DataSchema 13 | import com.fasterxml.jackson.annotation.JsonInclude 14 | import com.fasterxml.jackson.annotation.JsonInclude.Include.* 15 | import com.fasterxml.jackson.annotation.JsonProperty 16 | 17 | data class ThingAction( 18 | @JsonInclude(NON_EMPTY) 19 | override var title: String? = null, 20 | 21 | @JsonInclude(NON_EMPTY) 22 | override var description: String? = null, 23 | 24 | @JsonInclude(NON_EMPTY) 25 | override var descriptions: MutableMap? = null, 26 | 27 | @JsonInclude(NON_EMPTY) 28 | override var uriVariables: MutableMap>? = null, 29 | 30 | @JsonInclude(NON_EMPTY) 31 | override var forms: MutableList
= mutableListOf(), 32 | 33 | @JsonProperty("@type") 34 | @JsonInclude(NON_EMPTY) 35 | override var objectType: Type? = null, 36 | 37 | @JsonInclude(NON_NULL) 38 | override var input: DataSchema? = null, 39 | 40 | @JsonInclude(NON_NULL) 41 | override var output: DataSchema? = null, 42 | 43 | @JsonInclude(NON_DEFAULT) 44 | override var safe: Boolean = false, 45 | 46 | @JsonInclude(NON_DEFAULT) 47 | override var idempotent: Boolean = false, 48 | 49 | @JsonInclude(NON_NULL) 50 | override var synchronous: Boolean? = null, 51 | 52 | @JsonInclude(NON_EMPTY) 53 | override var titles: MutableMap? = null 54 | ) : ActionAffordance { 55 | } 56 | 57 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/TypeDeserializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing 8 | 9 | import org.eclipse.thingweb.thing.schema.Type 10 | import com.fasterxml.jackson.core.JsonParser 11 | import com.fasterxml.jackson.core.JsonToken 12 | import com.fasterxml.jackson.databind.DeserializationContext 13 | import com.fasterxml.jackson.databind.JsonDeserializer 14 | import com.fasterxml.jackson.databind.node.ArrayNode 15 | import com.fasterxml.jackson.databind.node.TextNode 16 | import org.slf4j.LoggerFactory 17 | import java.io.IOException 18 | 19 | 20 | class TypeDeserializer : JsonDeserializer() { 21 | 22 | @Throws(IOException::class) 23 | override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Type? { 24 | val t = p.currentToken() 25 | return when (t) { 26 | JsonToken.VALUE_STRING -> { 27 | Type(p.valueAsString) 28 | } 29 | JsonToken.START_ARRAY -> { 30 | val type = Type() 31 | val arrayNode = p.codec.readTree(p) 32 | val arrayElements = arrayNode.elements() 33 | while (arrayElements.hasNext()) { 34 | val arrayElement = arrayElements.next() 35 | if (arrayElement is TextNode) { 36 | type.addType(arrayElement.asText()) 37 | } 38 | } 39 | type 40 | } 41 | else -> { 42 | log.warn("Unable to deserialize Context of type '{}'", t) 43 | null 44 | } 45 | } 46 | } 47 | 48 | companion object { 49 | private val log = LoggerFactory.getLogger(TypeDeserializer::class.java) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/event/ThingEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.event 8 | 9 | import org.eclipse.thingweb.thing.schema.Type 10 | import org.eclipse.thingweb.thing.form.Form 11 | import org.eclipse.thingweb.thing.schema.DataSchema 12 | import org.eclipse.thingweb.thing.schema.EventAffordance 13 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 14 | import com.fasterxml.jackson.annotation.JsonInclude 15 | import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY 16 | import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL 17 | import com.fasterxml.jackson.annotation.JsonProperty 18 | 19 | @JsonIgnoreProperties(ignoreUnknown = true) 20 | data class ThingEvent( 21 | @JsonInclude(NON_EMPTY) 22 | override var title: String? = null, 23 | 24 | @JsonProperty("@type") 25 | @JsonInclude(NON_NULL) 26 | override var objectType: Type? = null, 27 | 28 | @JsonInclude(NON_NULL) 29 | override var data: DataSchema? = null, 30 | 31 | @JsonInclude(NON_EMPTY) 32 | override var description: String? = null, 33 | 34 | @JsonInclude(NON_EMPTY) 35 | override var descriptions: MutableMap? = null, 36 | 37 | @JsonInclude(NON_EMPTY) 38 | override var uriVariables: MutableMap>? = null, 39 | 40 | @JsonInclude(NON_EMPTY) 41 | override var forms: MutableList = mutableListOf(), 42 | 43 | @JsonInclude(NON_EMPTY) 44 | override var subscription: DataSchema? = null, 45 | 46 | @JsonInclude(NON_EMPTY) 47 | override var cancellation: DataSchema? = null, 48 | 49 | @JsonInclude(NON_EMPTY) 50 | override var titles: MutableMap? = null 51 | 52 | ) : EventAffordance 53 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/schema/Exentions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.schema 8 | 9 | import org.eclipse.thingweb.JsonMapper 10 | import com.fasterxml.jackson.databind.JsonNode 11 | 12 | // Extension function for creating an InteractionInput.Value from a string 13 | fun String.toInteractionInputValue(): InteractionInput.Value { 14 | return InteractionInput.Value(JsonMapper.instance.valueToTree((this))) 15 | } 16 | 17 | fun MutableMap<*, *>.toInteractionInputValue(): InteractionInput.Value { 18 | return InteractionInput.Value(JsonMapper.instance.valueToTree((this))) 19 | } 20 | 21 | fun MutableList<*>.toInteractionInputValue(): InteractionInput.Value { 22 | return InteractionInput.Value(JsonMapper.instance.valueToTree((this))) 23 | } 24 | 25 | fun Boolean.toInteractionInputValue(): InteractionInput.Value { 26 | return InteractionInput.Value(JsonMapper.instance.valueToTree((this))) 27 | } 28 | 29 | fun String.toDataSchemeValue(): JsonNode { 30 | return JsonMapper.instance.valueToTree((this)) 31 | } 32 | 33 | 34 | fun Boolean.toDataSchemeValue(): JsonNode { 35 | return JsonMapper.instance.valueToTree((this)) 36 | } 37 | 38 | 39 | fun Number.toInteractionInputValue(): InteractionInput.Value { 40 | return InteractionInput.Value(JsonMapper.instance.valueToTree((this))) 41 | } 42 | fun Number.toDataSchemeValue(): JsonNode { 43 | return JsonMapper.instance.valueToTree((this)) 44 | } 45 | 46 | fun Int.toInteractionInputValue(): InteractionInput.Value { 47 | return InteractionInput.Value(JsonMapper.instance.valueToTree((this))) 48 | } 49 | 50 | fun Int.toDataSchemeValue(): JsonNode { 51 | return JsonMapper.instance.valueToTree((this)) 52 | } 53 | 54 | fun List<*>.toDataSchemeValue(): JsonNode { 55 | return JsonMapper.instance.valueToTree((this)) 56 | } 57 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/security/BearerSecurityScheme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.security 8 | 9 | import com.fasterxml.jackson.annotation.JsonInclude 10 | 11 | /** 12 | * Bearer token authentication security configuration identified by the term bearer (i.e., "scheme": 13 | * "bearer"). This scheme is intended for situations where bearer tokens are used independently of 14 | * OAuth2. If the oauth2 scheme is specified it is not generally necessary to specify this scheme as 15 | * well as it is implied. For format, the value jwt indicates conformance with RFC7519, jws 16 | * indicates conformance with RFC7797, cwt indicates conformance with RFC8392, and jwe indicates 17 | * conformance with !RFC7516, with values for alg interpreted consistently with those standards. 18 | * Other formats and algorithms for bearer tokens MAY be specified in vocabulary extensions.

See 19 | * also: https://www.w3.org/2019/wot/security#bearersecurityscheme 20 | */ 21 | class BearerSecurityScheme @JvmOverloads constructor( 22 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val `in`: String? = null, 23 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val alg: String? = null, 24 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val format: String? = null, 25 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val name: String? = null, 26 | @field:JsonInclude(JsonInclude.Include.NON_EMPTY) val authorization: String? = null 27 | ) : SecurityScheme { 28 | 29 | override fun toString(): String { 30 | return "BearerSecurityScheme{" + 31 | "in='" + `in` + '\'' + 32 | ", alg='" + alg + '\'' + 33 | ", format='" + format + '\'' + 34 | ", name='" + name + '\'' + 35 | ", authorization='" + authorization + '\'' + 36 | '}' 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/form/Operation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.form 8 | 9 | import com.fasterxml.jackson.annotation.JsonCreator 10 | import com.fasterxml.jackson.annotation.JsonValue 11 | 12 | /** 13 | * Enumeration representing different operation types for Property, Action, and Event Affordances. 14 | */ 15 | 16 | enum class Operation(private val tdValue: String) { 17 | // Properties 18 | READ_PROPERTY("readproperty"), 19 | WRITE_PROPERTY("writeproperty"), 20 | OBSERVE_PROPERTY("observeproperty"), 21 | UNOBSERVE_PROPERTY("unobserveproperty"), 22 | OBSERVE_ALL_PROPERTIES("observeallproperties"), 23 | UNOBSERVE_ALL_PROPERTIES("unobserveallproperties"), 24 | READ_ALL_PROPERTIES("readallproperties"), 25 | WRITE_ALL_PROPERTIES("writeallproperties"), 26 | READ_MULTIPLE_PROPERTIES("readmultipleproperties"), 27 | WRITE_MULTIPLE_PROPERTIES("writemultipleproperties"), 28 | 29 | // Events 30 | SUBSCRIBE_EVENT("subscribeevent"), 31 | UNSUBSCRIBE_EVENT("unsubscribeevent"), 32 | SUBSCRIBE_ALL_EVENTS("subscribeallevents"), 33 | UNSUBSCRIBE_ALL_EVENTS("unsubscribeallevents"), 34 | 35 | // Actions 36 | INVOKE_ACTION("invokeaction"), 37 | QUERY_ACTION("queryaction"), 38 | CANCEL_ACTION("cancelaction"), 39 | QUERY_ALL_ACTIONS("queryallactions"); 40 | 41 | @JsonValue 42 | fun toJsonValue(): String { 43 | return tdValue 44 | } 45 | 46 | companion object { 47 | private val LOOKUP: MutableMap = HashMap() 48 | 49 | init { 50 | for (operation in entries) { 51 | LOOKUP[operation.toJsonValue()] = operation 52 | } 53 | } 54 | 55 | @JsonCreator 56 | fun fromJsonValue(jsonValue: String): Operation { 57 | return LOOKUP[jsonValue] ?: throw IllegalArgumentException("Unknown Operation: $jsonValue") 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /kotlin-wot-tool-example/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.run.BootRun 2 | import java.net.URI 3 | 4 | plugins { 5 | kotlin("plugin.spring") version "2.1.20" 6 | id("org.springframework.boot") version "3.1.5" // Use the latest compatible version 7 | id("io.spring.dependency-management") version "1.1.3" 8 | } 9 | 10 | dependencies { 11 | api(project(":kotlin-wot-binding-http")) 12 | api(project(":kotlin-wot-binding-websocket")) 13 | api(project(":kotlin-wot-spring-boot-starter")) 14 | api(project(":kotlin-wot-lmos-protocol")) 15 | implementation("org.jsoup:jsoup:1.7.2") 16 | } 17 | 18 | springBoot { 19 | mainClass.set("org.eclipse.thingweb.example.ToolApplicationKt") 20 | } 21 | 22 | tasks.register("downloadOtelAgent") { 23 | doLast { 24 | val agentUrl = 25 | "https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar" 26 | val agentFile = file("${project.buildDir}/libs/opentelemetry-javaagent.jar") 27 | 28 | // Ensure directory exists before downloading 29 | agentFile.parentFile.mkdirs() 30 | 31 | if (!agentFile.exists()) { 32 | println("Downloading OpenTelemetry Java Agent...") 33 | agentFile.writeBytes(URI(agentUrl).toURL().readBytes()) 34 | println("Download completed: ${agentFile.absolutePath}") 35 | } else { 36 | println("OpenTelemetry Java Agent already exists: ${agentFile.absolutePath}") 37 | } 38 | } 39 | } 40 | 41 | tasks.named("bootRun") { 42 | dependsOn("downloadOtelAgent") 43 | jvmArgs = listOf( 44 | "-javaagent:${project.buildDir}/libs/opentelemetry-javaagent.jar" 45 | ) 46 | systemProperty("otel.java.global-autoconfigure.enabled", "true") 47 | systemProperty("otel.traces.exporter", "otlp") 48 | systemProperty("otel.exporter.otlp.endpoint", "http://localhost:4318") 49 | systemProperty("otel.service.name", "scraper-tool") 50 | systemProperty("otel.javaagent.debug", "true") 51 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/schema/Link.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.schema 8 | 9 | import com.fasterxml.jackson.annotation.JsonInclude 10 | 11 | /** 12 | * Represents a link or submission target of a form with metadata that describes the link. 13 | * 14 | * @property href The target IRI of the link or submission target of a form. This is a mandatory field. 15 | * @property type An optional hint indicating the media type RFC2046 of the result of dereferencing the link. 16 | * @property rel An optional link relation type that identifies the semantics of a link. 17 | * @property anchor An optional override for the link context, with the given URI or IRI. By default, the context is the Thing itself identified by its ID. 18 | * @property sizes An optional target attribute specifying one or more sizes for the referenced icon. Only applicable if the relation type is "icon". 19 | * The format is {Height}x{Width} (e.g., "16x16", "16x16 32x32"). 20 | * @property hreflang An optional attribute specifying the language of a linked document. The value should be a valid language tag as per [BCP47]. 21 | * Can be a single string or an array of strings. 22 | */ 23 | data class Link( 24 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 25 | val href: String, // anyURI, mandatory 26 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 27 | val type: String? = null, // Optional media type hint 28 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 29 | val rel: String? = null, // Optional link relation type 30 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 31 | val anchor: String? = null, // Optional override for link context with URI or IRI 32 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 33 | val sizes: String? = null, // Optional sizes for the referenced icon, applicable if rel = "icon" 34 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 35 | val hreflang: List? = null // Optional list of valid language tags 36 | ) -------------------------------------------------------------------------------- /kotlin-wot-reflection/src/test/kotlin/reflection/things/SimpleThing.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.reflection.things 8 | 9 | import org.eclipse.thingweb.reflection.annotations.* 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.flow 13 | 14 | @Thing( 15 | id = "simpleThing", 16 | title = "Simple Thing", 17 | description = "A thing with complex properties, actions, and events." 18 | ) 19 | @Link( 20 | href = "my/link", 21 | rel = "my-rel", 22 | type = "my/type", 23 | anchor = "my-anchor", 24 | sizes = "my-sizes", 25 | hreflang = ["my-lang-1", "my-lang-2"] 26 | ) 27 | @VersionInfo(instance = "1.0.0") 28 | class SimpleThing { 29 | 30 | var counter = 0 31 | 32 | @Property(title = "Observable Property", readOnly = true) 33 | val observableProperty : MutableStateFlow = MutableStateFlow("Hello World") 34 | 35 | @Property() 36 | var mutableProperty: String = "test" 37 | 38 | @Property(readOnly = true) 39 | val readyOnlyProperty: String = "test" 40 | 41 | @Property(writeOnly = true) 42 | var writeOnlyProperty: String = "test" 43 | 44 | @Action() 45 | fun voidAction() { 46 | println("Action executed") 47 | counter += 1 48 | } 49 | 50 | @Action() 51 | fun changeObservableProperty(){ 52 | observableProperty.value = "Hello from action!" 53 | } 54 | 55 | @Action() 56 | fun outputAction() : String { 57 | return "test" 58 | } 59 | 60 | @Action() 61 | fun inputAction(input : String) { 62 | println("Action executed") 63 | counter += 1 64 | } 65 | 66 | @Action() 67 | fun inOutAction(input : String) : String { 68 | println("Action executed") 69 | return "$input output" 70 | } 71 | 72 | @Event() 73 | fun statusUpdated(): Flow { 74 | return flow { 75 | emit("Status updated") 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/content/JsonCodec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.content 8 | 9 | import org.eclipse.thingweb.JsonMapper 10 | import org.eclipse.thingweb.thing.schema.DataSchema 11 | import com.fasterxml.jackson.core.JsonProcessingException 12 | import com.fasterxml.jackson.databind.JsonNode 13 | import java.io.IOException 14 | import kotlin.reflect.KClass 15 | 16 | /** 17 | * (De)serializes data in JSON format. 18 | */ 19 | open class JsonCodec : ContentCodec { 20 | 21 | override val mediaType: String 22 | get() = "application/json" 23 | 24 | override fun bytesToValue( 25 | body: ByteArray, 26 | schema: DataSchema<*>?, 27 | parameters: Map 28 | ): JsonNode { 29 | return try { 30 | JsonMapper.instance.readTree(body) 31 | } catch (e: IOException) { 32 | throw ContentCodecException("Failed to decode $mediaType: ${e.message}", e) 33 | } 34 | } 35 | 36 | override fun bytesToValue(body: ByteArray, parameters: Map, clazz: KClass): O { 37 | return try { 38 | JsonMapper.instance.readValue(body, clazz.java) 39 | } catch (e: IOException) { 40 | throw ContentCodecException("Failed to decode $mediaType: ${e.message}", e) 41 | } 42 | } 43 | 44 | override fun valueToBytes( 45 | value: Any, 46 | parameters: Map 47 | ): ByteArray { 48 | return try { 49 | JsonMapper.instance.writeValueAsBytes(value) 50 | } catch (e: JsonProcessingException) { 51 | throw ContentCodecException("Failed to encode $mediaType: $e") 52 | } 53 | } 54 | 55 | override fun valueToBytes( 56 | value: JsonNode, 57 | parameters: Map 58 | ): ByteArray { 59 | try{ 60 | return JsonMapper.instance.writeValueAsBytes(value) 61 | } catch (e: JsonProcessingException) { 62 | throw ContentCodecException("Failed to encode $mediaType: $e") 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /kotlin-wot-spring-boot-starter/src/main/kotlin/spring/CredentialsProperties.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.spring 8 | 9 | 10 | import org.eclipse.thingweb.credentials.ApiKeyCredentials 11 | import org.eclipse.thingweb.credentials.BasicCredentials 12 | import org.eclipse.thingweb.credentials.BearerCredentials 13 | import org.springframework.boot.context.properties.ConfigurationProperties 14 | import org.springframework.validation.annotation.Validated 15 | 16 | @ConfigurationProperties(prefix = "wot.servient.security") 17 | @Validated 18 | data class CredentialsProperties( 19 | var credentials: Map = emptyMap() 20 | ) 21 | 22 | data class Credentials( 23 | val type: String, // to differentiate the credential type 24 | val token: String? = null, // for Bearer credentials 25 | val username: String? = null, // for Basic credentials 26 | val password: String? = null, // for Basic credentials 27 | val apiKey: String? = null // for API Key credentials 28 | ) { 29 | fun convert(): org.eclipse.thingweb.credentials.Credentials { 30 | return when (type) { 31 | "bearer" -> { 32 | if (token != null) { 33 | BearerCredentials(token) 34 | } else { 35 | throw IllegalArgumentException("Token is required for bearer credentials") 36 | } 37 | } 38 | "basic" -> { 39 | if (username != null && password != null) { 40 | BasicCredentials(username, password) 41 | } else { 42 | throw IllegalArgumentException("Username and password are required for basic credentials") 43 | } 44 | } 45 | "apikey" -> { 46 | if (apiKey != null) { 47 | ApiKeyCredentials(apiKey) 48 | } else { 49 | throw IllegalArgumentException("API Key is required for API Key credentials") 50 | } 51 | } 52 | else -> throw IllegalArgumentException("Unknown credentials type: $type") 53 | } 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/ContextDeserializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing 8 | 9 | import org.eclipse.thingweb.thing.schema.Context 10 | import com.fasterxml.jackson.core.JsonParser 11 | import com.fasterxml.jackson.core.JsonToken 12 | import com.fasterxml.jackson.databind.DeserializationContext 13 | import com.fasterxml.jackson.databind.JsonDeserializer 14 | import com.fasterxml.jackson.databind.node.ArrayNode 15 | import com.fasterxml.jackson.databind.node.ObjectNode 16 | import com.fasterxml.jackson.databind.node.TextNode 17 | import org.slf4j.LoggerFactory 18 | import java.io.IOException 19 | 20 | /** 21 | * Deserializes the individual context or the list of contexts of a [ThingDescription] from JSON. Is used 22 | * by Jackson 23 | */ 24 | internal class ContextDeserializer : JsonDeserializer() { 25 | @Throws(IOException::class) 26 | override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Context? { 27 | val t = p.currentToken() 28 | return if (t == JsonToken.VALUE_STRING) { 29 | Context(p.valueAsString) 30 | } else if (t == JsonToken.START_ARRAY) { 31 | val context = Context() 32 | val arrayNode = p.codec.readTree(p) 33 | val arrayElements = arrayNode.elements() 34 | while (arrayElements.hasNext()) { 35 | val arrayElement = arrayElements.next() 36 | if (arrayElement is TextNode) { 37 | context.addContext(arrayElement.asText()) 38 | } else if (arrayElement is ObjectNode) { 39 | val objectEntries = arrayElement.fields() 40 | while (objectEntries.hasNext()) { 41 | val (prefix, value) = objectEntries.next() 42 | val url = value.asText() 43 | context.addContext(prefix, url) 44 | } 45 | } 46 | } 47 | context 48 | } else { 49 | log.warn("Unable to deserialize Context of type '{}'", t) 50 | null 51 | } 52 | } 53 | 54 | companion object { 55 | private val log = LoggerFactory.getLogger(ContextDeserializer::class.java) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/Utils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb 8 | 9 | import org.eclipse.thingweb.thing.ThingDescription 10 | import org.eclipse.thingweb.thing.schema.InteractionAffordance 11 | import org.eclipse.thingweb.thing.schema.InteractionOptions 12 | import org.eclipse.thingweb.thing.schema.WoTThingDescription 13 | import org.eclipse.thingweb.thing.validateInteractionOptions 14 | import com.fasterxml.jackson.databind.ObjectMapper 15 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule 16 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 17 | import com.fasterxml.jackson.module.kotlin.registerKotlinModule 18 | 19 | object JsonMapper { 20 | val instance: ObjectMapper = jacksonObjectMapper().apply { 21 | registerKotlinModule() 22 | registerModule(JavaTimeModule()) 23 | } 24 | } 25 | 26 | fun parseInteractionOptions( 27 | thing: WoTThingDescription, 28 | ti: InteractionAffordance, 29 | options: InteractionOptions? = null 30 | ): InteractionOptions { 31 | 32 | require(validateInteractionOptions(thing, ti, options)) { 33 | "One or more uriVariables were not found under either '${ti.title}' Thing Interaction or '${thing.title}' Thing" 34 | } 35 | 36 | val interactionUriVariables = ti.uriVariables ?: emptyMap() 37 | val thingUriVariables = thing.uriVariables ?: emptyMap() 38 | val uriVariables = mutableMapOf() 39 | options?.uriVariables?.let { userUriVariables -> 40 | userUriVariables.forEach { (key, value) -> 41 | if (key in interactionUriVariables || key in thingUriVariables) { 42 | uriVariables[key] = value 43 | } 44 | } 45 | } 46 | /* 47 | thingUriVariables.forEach { (key, value) -> 48 | if (key !in uriVariables && value is Map<*, *> && "default" in value) { 49 | uriVariables[key] = value["default"] 50 | } 51 | } 52 | */ 53 | 54 | return InteractionOptions(uriVariables = uriVariables.toMap()) 55 | } 56 | 57 | fun validateInteractionOptions( 58 | thingDescription: ThingDescription, 59 | ti: InteractionAffordance, 60 | options: InteractionOptions? = null 61 | ): Boolean { 62 | val interactionUriVariables = ti.uriVariables ?: emptyMap() 63 | val thingUriVariables = thingDescription.uriVariables ?: emptyMap() 64 | 65 | return options?.uriVariables?.all { (key, _) -> 66 | key in interactionUriVariables || key in thingUriVariables 67 | } ?: true 68 | } -------------------------------------------------------------------------------- /kotlin-wot/src/test/kotlin/thing/UriTemplateTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing 8 | 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | import kotlin.test.assertFailsWith 12 | import kotlin.test.assertTrue 13 | 14 | class UriTemplateTest { 15 | 16 | @Test 17 | fun expandReplacesPlaceholdersWithValues() { 18 | val template = UriTemplate.fromTemplate("/things/{thingId}/properties/{propertyName}") 19 | val uriVariables = mapOf("thingId" to "123", "propertyName" to "456") 20 | val result = template.expand(uriVariables) 21 | assertEquals("/things/123/properties/456", result) 22 | } 23 | 24 | @Test 25 | fun expandThrowsExceptionForUnresolvedPlaceholders() { 26 | val template = UriTemplate.fromTemplate("/things/{thingId}/properties/{propertyName}") 27 | val uriVariables = mapOf("thingId" to "123") 28 | val exception = assertFailsWith { 29 | template.expand(uriVariables) 30 | } 31 | assertTrue(exception.message!!.contains("unresolved placeholders")) 32 | } 33 | 34 | @Test 35 | fun expandHandlesEmptyTemplate() { 36 | val template = UriTemplate.fromTemplate("") 37 | val uriVariables = mapOf("thingId" to "123") 38 | val result = template.expand(uriVariables) 39 | assertEquals("", result) 40 | } 41 | 42 | @Test 43 | fun expandHandlesNoPlaceholders() { 44 | val template = UriTemplate.fromTemplate("/things/properties/all") 45 | val uriVariables = mapOf("thingId" to "123") 46 | val result = template.expand(uriVariables) 47 | assertEquals("/things/properties/all", result) 48 | } 49 | 50 | @Test 51 | fun expandHandlesEmptyUriVariables() { 52 | val template = UriTemplate.fromTemplate("/things/{thingId}/properties/{propertyName}") 53 | val uriVariables = emptyMap() 54 | val exception = assertFailsWith { 55 | template.expand(uriVariables) 56 | } 57 | assertTrue(exception.message!!.contains("unresolved placeholders")) 58 | } 59 | 60 | @Test 61 | fun expandHandlesQueryVariablesInPath() { 62 | val template = UriTemplate.fromTemplate("/search?query={query}&page={page}") 63 | val uriVariables = mapOf("query" to "kotlin", "page" to "1") 64 | val result = template.expand(uriVariables) 65 | assertEquals("/search?query=kotlin&page=1", result) 66 | } 67 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/credentials/DefaultCredentialsProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.credentials 8 | 9 | 10 | import org.eclipse.thingweb.security.* 11 | import org.eclipse.thingweb.thing.schema.WoTForm 12 | import org.slf4j.LoggerFactory 13 | 14 | class DefaultCredentialsProvider( 15 | private val securitySchemes: List, 16 | private val credentials: Map 17 | ) : CredentialsProvider { 18 | 19 | private val log = LoggerFactory.getLogger(DefaultCredentialsProvider::class.java) 20 | 21 | override fun getCredentials(form: WoTForm): Credentials? { 22 | if (securitySchemes.isEmpty()) { 23 | return null 24 | } 25 | return when (val security = securitySchemes.firstOrNull()) { 26 | is BasicSecurityScheme -> { 27 | val matchedCredentials = getMatchedCredentials(form) 28 | if (matchedCredentials is BasicCredentials) { 29 | matchedCredentials 30 | } else { 31 | throw NoCredentialsFound("Expected BasicCredentials but found ${matchedCredentials::class.simpleName}") 32 | } 33 | } 34 | 35 | is BearerSecurityScheme, is OAuth2SecurityScheme -> { 36 | val matchedCredentials = getMatchedCredentials(form) 37 | if (matchedCredentials is BearerCredentials) { 38 | matchedCredentials 39 | } else { 40 | throw NoCredentialsFound("Expected BearerCredentials but found ${matchedCredentials::class.simpleName}") 41 | } 42 | } 43 | 44 | is NoSecurityScheme -> null 45 | else -> { 46 | log.error("Cannot set security scheme '{}'", security) 47 | null 48 | } 49 | } 50 | } 51 | 52 | private fun getMatchedCredentials(form: WoTForm): Credentials { 53 | // Find the first credential where the key (URI) is contained in href 54 | val matchedCredentialKey = credentials.keys.firstOrNull { key -> form.href.contains(key) } 55 | val matchedCredentials = matchedCredentialKey?.let { credentials[it] } 56 | 57 | if (matchedCredentials == null) { 58 | throw NoCredentialsFound("No matching credentials found for href: ${form.href}") 59 | } 60 | return matchedCredentials 61 | 62 | } 63 | 64 | } 65 | 66 | class NoCredentialsFound(message: String) : RuntimeException(message) 67 | -------------------------------------------------------------------------------- /kotlin-wot/src/test/kotlin/thing/action/ThingActionTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.action 8 | 9 | import org.eclipse.thingweb.JsonMapper 10 | import org.eclipse.thingweb.thing.schema.StringSchema 11 | import com.fasterxml.jackson.module.kotlin.readValue 12 | import net.javacrumbs.jsonunit.assertj.JsonAssertions 13 | import net.javacrumbs.jsonunit.core.Option 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | 17 | 18 | class ThingActionTest { 19 | @Test 20 | fun testEquals() { 21 | val input= StringSchema() 22 | val output= StringSchema() 23 | 24 | val action1 = ThingAction(input = input, output = output) 25 | val action2 = ThingAction(input = input, output = output) 26 | assertEquals(action1, action2) 27 | } 28 | 29 | @Test 30 | fun testHashCode() { 31 | val input= StringSchema() 32 | val output= StringSchema() 33 | 34 | val action1 = ThingAction(input = input, output = output).hashCode() 35 | val action2 = ThingAction(input = input, output = output).hashCode() 36 | assertEquals(action1, action2) 37 | } 38 | 39 | @Test 40 | fun testToJson() { 41 | val action = ThingAction( 42 | title = "title", 43 | description = "blabla", 44 | input = StringSchema(), 45 | output = StringSchema()) 46 | val json = JsonMapper.instance.writeValueAsString(action) 47 | 48 | JsonAssertions.assertThatJson(json) 49 | .`when`(Option.IGNORING_ARRAY_ORDER) 50 | .isEqualTo( 51 | """{ 52 | "title":"title", 53 | "description":"blabla", 54 | "input":{"type":"string"}, 55 | "output":{"type":"string"} 56 | } 57 | """ 58 | ) 59 | } 60 | 61 | @Test 62 | fun fromJson() { 63 | val json = """{ 64 | "title":"title", 65 | "description":"blabla", 66 | "input":{"type":"string"}, 67 | "output":{"type":"string"} 68 | } 69 | """ 70 | 71 | val parsedAction = JsonMapper.instance.readValue>(json) 72 | val action = ThingAction( 73 | title = "title", 74 | description = "blabla", 75 | input = StringSchema(), 76 | output = StringSchema()) 77 | assertEquals(action, parsedAction) 78 | } 79 | 80 | 81 | } -------------------------------------------------------------------------------- /kotlin-wot/src/test/kotlin/thing/event/ThingEventTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.event 8 | 9 | import org.eclipse.thingweb.JsonMapper 10 | import org.eclipse.thingweb.thing.schema.StringSchema 11 | import com.fasterxml.jackson.module.kotlin.readValue 12 | import net.javacrumbs.jsonunit.assertj.JsonAssertions 13 | import net.javacrumbs.jsonunit.core.Option 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | 17 | class ThingEventTest { 18 | 19 | @Test 20 | fun testThingEvent() { 21 | // Assuming ThingEvent can be directly constructed instead of using Builder 22 | val event = ThingEvent( 23 | data = StringSchema(title = "title") 24 | ) 25 | assertEquals("title", event.data?.title) 26 | } 27 | 28 | 29 | @Test 30 | fun testToJson() { 31 | val event = ThingEvent( 32 | title = "event", 33 | data = StringSchema() 34 | ) 35 | val json = JsonMapper.instance.writeValueAsString(event) 36 | 37 | JsonAssertions.assertThatJson(json) 38 | .`when`(Option.IGNORING_ARRAY_ORDER) 39 | .isEqualTo( 40 | """{ 41 | "title": "event", 42 | "data": 43 | {"type":"string"} 44 | } 45 | """ 46 | ) 47 | } 48 | 49 | @Test 50 | fun fromJson() { 51 | val json = """{ 52 | "title": "event", 53 | "data": 54 | {"type":"string"} 55 | } 56 | """ 57 | 58 | val parsedEvent = JsonMapper.instance.readValue>(json) 59 | val event = ThingEvent( 60 | title = "event", 61 | data = StringSchema()) 62 | assertEquals(event, parsedEvent) 63 | } 64 | 65 | @Test 66 | fun testEquals() { 67 | val data = StringSchema() 68 | 69 | val eventA = ThingEvent( 70 | data = data 71 | ) 72 | val eventB = ThingEvent( 73 | data = data 74 | ) 75 | assertEquals(eventA, eventB) 76 | } 77 | 78 | @Test 79 | fun testHashCode() { 80 | val data = StringSchema() 81 | 82 | val eventA = ThingEvent( 83 | data = data 84 | ).hashCode() 85 | val eventB = ThingEvent( 86 | data = data 87 | ).hashCode() 88 | assertEquals(eventA, eventB) 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/content/LinkFormatCodec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.content 8 | 9 | /* 10 | class LinkFormatCodec : ContentCodec { 11 | private val log = LoggerFactory.getLogger(LinkFormatCodec::class.java) 12 | override val mediaType: String 13 | get() = "application/link-format" 14 | 15 | override fun bytesToValue( 16 | body: ByteArray, 17 | schema: DataSchema, 18 | parameters: Map 19 | ): T { 20 | val pattern = Regex("""^(.+)="(.+)"""") 21 | 22 | return when (schema) { 23 | is ObjectSchema -> { 24 | val entries = mutableMapOf>() 25 | val bodyString = body.toString(Charsets.UTF_8) // Decode the byte array to a string 26 | val entriesStrings = bodyString.split(",").filter { it.isNotEmpty() } // Split and filter empty entries 27 | 28 | for (entryString in entriesStrings) { 29 | val entryComponents = entryString.split(";", limit = 4).toMutableList() 30 | val entryKey = entryComponents.removeAt(0) 31 | val entryParameters = mutableMapOf() 32 | 33 | entryComponents.forEach { component -> 34 | val matchResult = pattern.matchEntire(component) 35 | matchResult?.let { 36 | val key = it.groups[1]?.value 37 | val value = it.groups[2]?.value 38 | if (key != null && value != null) { 39 | entryParameters[key] = value 40 | } 41 | } 42 | } 43 | entries[entryKey] = entryParameters 44 | } 45 | entries as T // Safe cast, since we know the structure 46 | } 47 | else -> throw ContentCodecException("Non-object data schema not implemented yet") 48 | } 49 | } 50 | 51 | override fun valueToBytes(value: Any, parameters: Map): ByteArray { 52 | return if (value is Map<*, *>) { 53 | val valueMap = value as Map> 54 | val bodyString = valueMap.entries.stream().map { (key, value1): Map.Entry> -> 55 | "$key;" + value1.entries.stream() 56 | .map { (key1, value2): Map.Entry -> "$key1=\"$value2\"" } 57 | .collect(Collectors.joining(";")) 58 | }.collect(Collectors.joining(",")) 59 | bodyString.toByteArray() 60 | } else { 61 | log.warn("Unable to serialize non-map value: {}", value) 62 | ByteArray(0) 63 | } 64 | } 65 | } 66 | */ 67 | -------------------------------------------------------------------------------- /kotlin-wot-spring-boot-starter/src/main/kotlin/spring/WoTRuntime.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.spring 8 | 9 | import org.eclipse.thingweb.Servient 10 | import org.eclipse.thingweb.Wot 11 | import org.eclipse.thingweb.reflection.ExposedThingBuilder 12 | import org.eclipse.thingweb.reflection.annotations.Thing 13 | import org.eclipse.thingweb.thing.schema.WoTExposedThing 14 | import jakarta.annotation.PreDestroy 15 | import kotlinx.coroutines.runBlocking 16 | import org.slf4j.Logger 17 | import org.slf4j.LoggerFactory 18 | import org.springframework.beans.factory.annotation.Autowired 19 | import org.springframework.beans.factory.getBeansWithAnnotation 20 | import org.springframework.boot.CommandLineRunner 21 | import org.springframework.context.ApplicationContext 22 | import java.util.concurrent.CountDownLatch 23 | import kotlin.reflect.KClass 24 | 25 | 26 | class WoTRuntime : CommandLineRunner { 27 | 28 | @Autowired 29 | private lateinit var servient: Servient 30 | 31 | @Autowired 32 | private lateinit var wot: Wot 33 | 34 | @Autowired 35 | private lateinit var applicationContext: ApplicationContext 36 | 37 | private val latch = CountDownLatch(1) 38 | 39 | companion object { 40 | private val log: Logger = LoggerFactory.getLogger(WoTRuntime::class.java) 41 | } 42 | 43 | @PreDestroy 44 | fun onExit() { 45 | // Register a shutdown hook 46 | log.debug("Application is shutting down. Performing cleanup...") 47 | runBlocking { servient.shutdown() } 48 | latch.countDown(); 49 | } 50 | 51 | override fun run(vararg args: String?) = runBlocking { 52 | 53 | val things = applicationContext.getBeansWithAnnotation() 54 | 55 | try{ 56 | servient.start() 57 | for (thing in things.values) { 58 | // Cast clazz to KClass explicitly 59 | @Suppress("UNCHECKED_CAST") 60 | val typedClass = thing::class as KClass 61 | val exposedThing = ExposedThingBuilder.createExposedThing(wot, thing, typedClass) 62 | // Add and expose the thing after `start()` has had time to begin 63 | servient.addThing(exposedThing as WoTExposedThing) 64 | servient.expose(exposedThing.getThingDescription().id) 65 | } 66 | 67 | log.info("Application is running... Press Ctrl+C to exit."); 68 | 69 | val awaitThread: Thread = object : Thread() { 70 | override fun run() { 71 | latch.await() 72 | } 73 | } 74 | awaitThread.contextClassLoader = javaClass.classLoader 75 | awaitThread.isDaemon = false 76 | awaitThread.start() 77 | } catch (e : Exception){ 78 | log.warn("Failed to start WoTRuntime", e) 79 | // Ensure not server is left running 80 | servient.shutdown() 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/DefaultWot.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb 8 | 9 | 10 | import org.eclipse.thingweb.security.SecurityScheme 11 | import org.eclipse.thingweb.thing.ConsumedThing 12 | import org.eclipse.thingweb.thing.ExposedThing 13 | import org.eclipse.thingweb.thing.ThingDescription 14 | import org.eclipse.thingweb.thing.filter.DiscoveryMethod 15 | import org.eclipse.thingweb.thing.filter.ThingFilter 16 | import org.eclipse.thingweb.thing.schema.WoTExposedThing 17 | import org.eclipse.thingweb.thing.schema.WoTThingDescription 18 | import io.opentelemetry.api.trace.SpanKind 19 | import io.opentelemetry.instrumentation.annotations.WithSpan 20 | import kotlinx.coroutines.flow.Flow 21 | import java.net.URI 22 | import java.net.URISyntaxException 23 | 24 | /** 25 | * Standard implementation of [Wot]. 26 | */ 27 | class DefaultWot(private val servient: Servient) : Wot { 28 | 29 | override fun toString(): String { 30 | return "DefaultWot{" + 31 | "servient=" + servient + 32 | '}' 33 | } 34 | @Throws(WotException::class) 35 | @WithSpan(kind = SpanKind.CLIENT) 36 | override fun discover(filter: ThingFilter): Flow { 37 | return servient.discover(filter) 38 | } 39 | 40 | @Throws(WotException::class) 41 | @WithSpan(kind = SpanKind.CLIENT) 42 | override fun discover(): Flow { 43 | return discover(ThingFilter(method = DiscoveryMethod.ANY)) 44 | } 45 | 46 | @WithSpan(kind = SpanKind.CLIENT) 47 | override suspend fun exploreDirectory(directoryUrl: String, securityScheme: SecurityScheme): Set { 48 | return servient.exploreDirectory(directoryUrl, securityScheme) 49 | } 50 | 51 | override fun produce(thingDescription: WoTThingDescription): WoTExposedThing { 52 | val exposedThing = ExposedThing(servient, thingDescription) 53 | return if (servient.addThing(exposedThing)) { 54 | exposedThing 55 | } else { 56 | throw WotException("Thing already exists: " + thingDescription.id) 57 | } 58 | } 59 | 60 | override fun produce(configure: ThingDescription.() -> Unit): WoTExposedThing { 61 | val thingDescription = ThingDescription().apply(configure) 62 | return produce(thingDescription) 63 | } 64 | 65 | override fun consume(thingDescription: WoTThingDescription) = ConsumedThing(servient, thingDescription) 66 | 67 | @WithSpan(kind = SpanKind.CLIENT) 68 | override suspend fun requestThingDescription(url: URI, securityScheme: SecurityScheme): WoTThingDescription { 69 | return servient.fetch(url, securityScheme) 70 | } 71 | 72 | @Throws(URISyntaxException::class) 73 | @WithSpan(kind = SpanKind.CLIENT) 74 | override suspend fun requestThingDescription(url: String, securityScheme: SecurityScheme): WoTThingDescription { 75 | return servient.fetch(url, securityScheme) 76 | } 77 | 78 | suspend fun destroy() { 79 | return servient.shutdown() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /kotlin-wot-reflection/src/test/kotlin/reflection/MapTypeToSchemaTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.reflection 8 | 9 | import org.eclipse.thingweb.reflection.ExposedThingBuilder.mapTypeToSchema 10 | import org.eclipse.thingweb.thing.schema.* 11 | import kotlin.reflect.typeOf 12 | import kotlin.test.Test 13 | import kotlin.test.assertEquals 14 | 15 | class MapTypeToSchemaTest { 16 | 17 | @Test 18 | fun testMapTypeToSchema_Primitives() { 19 | assertEquals(IntegerSchema(), mapTypeToSchema(typeOf())) 20 | assertEquals(NumberSchema(), mapTypeToSchema(typeOf())) 21 | assertEquals(NumberSchema(), mapTypeToSchema(typeOf())) 22 | assertEquals(NumberSchema(), mapTypeToSchema(typeOf())) 23 | assertEquals(StringSchema(), mapTypeToSchema(typeOf())) 24 | assertEquals(BooleanSchema(), mapTypeToSchema(typeOf())) 25 | assertEquals(NullSchema(), mapTypeToSchema(typeOf())) 26 | } 27 | 28 | @Test 29 | fun testMapTypeToSchema_Collection() { 30 | // List example 31 | val listType = typeOf>() 32 | assertEquals(ArraySchema(items = StringSchema()), mapTypeToSchema(listType)) 33 | 34 | // Set example 35 | val setType = typeOf>() 36 | assertEquals(ArraySchema(items = IntegerSchema()), mapTypeToSchema(setType)) 37 | } 38 | 39 | @Test 40 | fun testMapTypeToSchema_Array() { 41 | // Array example 42 | assertEquals(ArraySchema(items = StringSchema()), mapTypeToSchema(typeOf>())) 43 | 44 | // Array example 45 | assertEquals(ArraySchema(items = NumberSchema()), mapTypeToSchema(typeOf>())) 46 | 47 | // Array example 48 | assertEquals(ArraySchema(items = IntegerSchema()), mapTypeToSchema(typeOf>())) 49 | } 50 | 51 | @Test 52 | fun testMapTypeToSchema_CustomObject() { 53 | // Custom class mapping, e.g., MyClass 54 | // Assuming that buildObjectSchema() can handle custom classes 55 | val objectSchema = ObjectSchema().apply { 56 | properties["id"] = IntegerSchema() 57 | properties["name"] = StringSchema() 58 | required += listOf("id", "name") 59 | } 60 | 61 | assertEquals(objectSchema, mapTypeToSchema(typeOf())) 62 | } 63 | 64 | @Test 65 | fun testMapTypeToSchema_CustomObjectWithOptionalProperties() { 66 | // Custom class mapping, e.g., MyClass 67 | // Assuming that buildObjectSchema() can handle custom classes 68 | val objectSchema = ObjectSchema().apply { 69 | properties["id"] = IntegerSchema() 70 | properties["name"] = StringSchema() 71 | required += listOf("id") 72 | } 73 | 74 | assertEquals(objectSchema, mapTypeToSchema(typeOf())) 75 | } 76 | 77 | // Assuming the following simple class for testing: 78 | data class MyClass(val id: Int, val name: String) 79 | data class MyClassWithOptional(val id: Int, val name: String?) 80 | } -------------------------------------------------------------------------------- /kotlin-wot/src/test/kotlin/thing/ContextTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing 8 | 9 | 10 | import org.eclipse.thingweb.thing.schema.Context 11 | import com.fasterxml.jackson.core.JsonProcessingException 12 | import com.fasterxml.jackson.databind.ObjectMapper 13 | import java.io.IOException 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | 17 | class ContextTest { 18 | 19 | private val jsonMapper = ObjectMapper() 20 | 21 | /* 22 | @Test 23 | @Throws(JsonProcessingException::class) 24 | fun `test default url`() { 25 | val jsonString = Json.encodeToString(ContextSerializer, Context("http://www.w3.org/ns/td")) 26 | // single value 27 | assertEquals("\"http://www.w3.org/ns/td\"", jsonString) 28 | 29 | val deserializedContext = Json.decodeFromString(ContextSerializer, jsonString) 30 | } 31 | 32 | @Test 33 | fun `test with multiple context` () { 34 | val context = Context("http://www.w3.org/ns/td") 35 | .addContext("saref", "https://w3id.org/saref#") 36 | 37 | // Serialization 38 | val jsonString = Json.encodeToString(ContextSerializer, context) 39 | println(jsonString) 40 | 41 | assertEquals( 42 | "[\"http://www.w3.org/ns/td\",{\"saref\":\"https://w3id.org/saref#\"}]", 43 | jsonString 44 | ) 45 | 46 | // Deserialization 47 | val deserializedContext = Json.decodeFromString(ContextSerializer, jsonString) 48 | 49 | } 50 | */ 51 | 52 | @Test 53 | @Throws(IOException::class) 54 | fun fromJson() { 55 | // single value 56 | assertEquals( 57 | Context("http://www.w3.org/ns/td"), 58 | jsonMapper.readValue( 59 | "\"http://www.w3.org/ns/td\"", 60 | Context::class.java 61 | ) 62 | ) 63 | 64 | // array 65 | assertEquals( 66 | Context("http://www.w3.org/ns/td"), 67 | jsonMapper.readValue( 68 | "[\"http://www.w3.org/ns/td\"]", 69 | Context::class.java 70 | ) 71 | ) 72 | 73 | // multi type array 74 | assertEquals( 75 | Context("http://www.w3.org/ns/td") 76 | .addContext("saref", "https://w3id.org/saref#"), 77 | jsonMapper.readValue( 78 | "[\"http://www.w3.org/ns/td\",{\"saref\":\"https://w3id.org/saref#\"}]", 79 | Context::class.java 80 | ) 81 | ) 82 | } 83 | 84 | @Test 85 | @Throws(JsonProcessingException::class) 86 | fun toJson() { 87 | // single value 88 | assertEquals( 89 | "\"http://www.w3.org/ns/td\"", 90 | jsonMapper.writeValueAsString(Context("http://www.w3.org/ns/td")) 91 | ) 92 | 93 | // multi type array 94 | assertEquals( 95 | "[\"http://www.w3.org/ns/td\",{\"saref\":\"https://w3id.org/saref#\"}]", 96 | jsonMapper.writeValueAsString( 97 | Context("http://www.w3.org/ns/td") 98 | .addContext("saref", "https://w3id.org/saref#") 99 | ) 100 | ) 101 | } 102 | } -------------------------------------------------------------------------------- /kotlin-wot/src/test/kotlin/thing/ProtocolHelpersTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing 8 | 9 | 10 | import org.eclipse.thingweb.thing.form.Form 11 | import org.eclipse.thingweb.thing.form.Operation 12 | import org.eclipse.thingweb.thing.schema.StringProperty 13 | import kotlin.test.Test 14 | import kotlin.test.assertEquals 15 | 16 | class ProtocolHelpersTest { 17 | 18 | @Test 19 | fun findRequestMatchingFormIndexReturnsCorrectIndex() { 20 | val forms = listOf( 21 | Form(href = "http://example.com/users", contentType = "application/json"), 22 | Form(href = "http://example.com/users/{userId}", contentType = "application/json") 23 | ) 24 | val result = findRequestMatchingFormIndex(forms, "http", "users") 25 | assertEquals(0, result) 26 | } 27 | 28 | @Test 29 | fun findRequestMatchingFormIndexReturnsZeroForNullForms() { 30 | val result = findRequestMatchingFormIndex(null, "http", "users") 31 | assertEquals(0, result) 32 | } 33 | 34 | @Test 35 | fun findRequestMatchingFormIndexReturnsZeroForNoMatch() { 36 | val forms = listOf( 37 | Form(href = "http://example.com/users", contentType = "application/json") 38 | ) 39 | val result = findRequestMatchingFormIndex(forms, "http", "posts") 40 | assertEquals(0, result) 41 | } 42 | 43 | @Test 44 | fun getFormIndexForOperationReturnsCorrectIndex() { 45 | val interaction = StringProperty().apply { 46 | forms = mutableListOf( 47 | Form(href = "http://example.com/users", op = listOf(Operation.READ_PROPERTY)), 48 | Form(href = "http://example.com/users", op = listOf(Operation.WRITE_PROPERTY)) 49 | ) 50 | readOnly = false 51 | writeOnly = false 52 | } 53 | val result = getFormIndexForOperation(interaction, "property", Operation.WRITE_PROPERTY) 54 | assertEquals(1, result) 55 | } 56 | 57 | @Test 58 | fun getFormIndexForOperationReturnsMinusOneForInvalidFormIndex() { 59 | val interaction = StringProperty().apply { 60 | forms = mutableListOf( 61 | Form(href = "http://example.com/users", op = listOf(Operation.READ_PROPERTY)) 62 | ) 63 | readOnly = false 64 | writeOnly = false 65 | } 66 | val result = getFormIndexForOperation(interaction, "property", Operation.WRITE_PROPERTY, 1) 67 | assertEquals(-1, result) 68 | } 69 | 70 | @Test 71 | fun getPropertyOpValuesReturnsCorrectOperations() { 72 | val property = StringProperty().apply { 73 | forms = mutableListOf() 74 | readOnly = false 75 | writeOnly = false 76 | observable = true 77 | } 78 | val result = getPropertyOpValues(property) 79 | assertEquals(listOf(Operation.WRITE_PROPERTY, Operation.READ_PROPERTY, Operation.OBSERVE_PROPERTY, Operation.UNOBSERVE_PROPERTY), result) 80 | } 81 | 82 | @Test 83 | fun getPropertyOpValuesReturnsEmptyListForReadOnlyAndWriteOnly() { 84 | val property = StringProperty().apply { 85 | forms = mutableListOf() 86 | readOnly = true 87 | writeOnly = true 88 | observable = false 89 | } 90 | val result = getPropertyOpValues(property) 91 | assertEquals(emptyList(), result) 92 | } 93 | } -------------------------------------------------------------------------------- /kotlin-wot-reflection/src/main/kotlin/reflection/annotations/Annotations.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.reflection.annotations 8 | 9 | import org.eclipse.thingweb.thing.DEFAULT_TYPE 10 | 11 | @Target(AnnotationTarget.CLASS) 12 | @Retention(AnnotationRetention.RUNTIME) 13 | annotation class Thing(val id: String, val title: String, val description: String, val type : String= DEFAULT_TYPE) 14 | 15 | @Target(AnnotationTarget.CLASS) 16 | @Retention(AnnotationRetention.RUNTIME) 17 | @Repeatable 18 | annotation class Context(val prefix: String = "", val url : String) 19 | 20 | @Target(AnnotationTarget.CLASS) 21 | @Retention(AnnotationRetention.RUNTIME) 22 | annotation class VersionInfo(val instance : String, val model : String = "") 23 | 24 | @Target(AnnotationTarget.CLASS) 25 | @Retention(AnnotationRetention.RUNTIME) 26 | annotation class Link( 27 | val href: String, 28 | val type: String = "", 29 | val rel: String = "", 30 | val anchor: String = "", 31 | val sizes: String = "", 32 | val hreflang: Array = [] 33 | ) 34 | 35 | @Target(AnnotationTarget.CLASS) 36 | @Retention(AnnotationRetention.RUNTIME) 37 | annotation class Links(val values: Array) 38 | 39 | @Target(AnnotationTarget.PROPERTY) 40 | @Retention(AnnotationRetention.RUNTIME) 41 | annotation class Property(val title: String = "", val description: String = "", val readOnly: Boolean = false, val writeOnly: Boolean = false) 42 | 43 | @Target(AnnotationTarget.PROPERTY) 44 | @Retention(AnnotationRetention.RUNTIME) 45 | annotation class ObjectProperty(val title: String = "", val description: String = "") 46 | 47 | @Target(AnnotationTarget.FUNCTION) 48 | @Retention(AnnotationRetention.RUNTIME) 49 | annotation class Action(val title: String = "", val description: String = "", 50 | val safe : Boolean = false, val idempotent : Boolean = false, val synchronous: Boolean = true 51 | ) 52 | 53 | @Target(AnnotationTarget.FUNCTION) 54 | @Retention(AnnotationRetention.RUNTIME) 55 | annotation class ActionInput(val title: String = "", val description: String = "") 56 | 57 | @Target(AnnotationTarget.FUNCTION) 58 | @Retention(AnnotationRetention.RUNTIME) 59 | annotation class ActionOutput(val title: String = "", val description: String = "") 60 | 61 | @Target(AnnotationTarget.FUNCTION) 62 | @Retention(AnnotationRetention.RUNTIME) 63 | annotation class Event(val title: String = "", val description: String = "") 64 | 65 | @Target(AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) 66 | @Retention(AnnotationRetention.RUNTIME) 67 | annotation class IntegerSchema( 68 | val minimum: Int = Int.MIN_VALUE, 69 | val maximum: Int = Int.MAX_VALUE, 70 | val multipleOf: Int = 1 71 | ) 72 | 73 | @Target(AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) 74 | @Retention(AnnotationRetention.RUNTIME) 75 | annotation class NumberSchema( 76 | val minimum: Double = Double.NEGATIVE_INFINITY, 77 | val maximum: Double = Double.POSITIVE_INFINITY, 78 | val multipleOf: Double = 0.0 // 0.0 means "not restricted" 79 | ) 80 | 81 | @Target(AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) 82 | @Retention(AnnotationRetention.RUNTIME) 83 | annotation class StringSchema( 84 | val minLength: Int = 0, 85 | val maxLength: Int = Int.MAX_VALUE, 86 | val pattern: String = "" // Regular expression 87 | ) 88 | 89 | @Target(AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) 90 | @Retention(AnnotationRetention.RUNTIME) 91 | annotation class ArraySchema( 92 | val minItems: Int = 0, 93 | val maxItems: Int = Int.MAX_VALUE, 94 | val uniqueItems: Boolean = false 95 | ) -------------------------------------------------------------------------------- /kotlin-wot-spring-boot-starter/src/test/kotlin/spring/CredentialsPropertiesTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package spring 8 | 9 | import org.eclipse.thingweb.credentials.ApiKeyCredentials 10 | import org.eclipse.thingweb.credentials.BasicCredentials 11 | import org.eclipse.thingweb.credentials.BearerCredentials 12 | import org.eclipse.thingweb.spring.Credentials 13 | import kotlin.test.Test 14 | import kotlin.test.assertEquals 15 | import kotlin.test.assertFailsWith 16 | import kotlin.test.assertTrue 17 | 18 | class CredentialsPropertiesTest { 19 | 20 | @Test 21 | fun `test convert with bearer credentials`() { 22 | val credentials = Credentials(type = "bearer", token = "sample_token") 23 | 24 | val convertedCredentials = credentials.convert() 25 | 26 | // Ensure the converted credentials is of type BearerCredentials and the token matches 27 | assertTrue(convertedCredentials is BearerCredentials) 28 | assertEquals("sample_token", (convertedCredentials as BearerCredentials).token) 29 | } 30 | 31 | @Test 32 | fun `test convert with basic credentials`() { 33 | val credentials = Credentials(type = "basic", username = "user", password = "pass") 34 | 35 | val convertedCredentials = credentials.convert() 36 | 37 | // Ensure the converted credentials is of type BasicCredentials 38 | assertTrue(convertedCredentials is BasicCredentials) 39 | val basicCredentials = convertedCredentials as BasicCredentials 40 | assertEquals("user", basicCredentials.username) 41 | assertEquals("pass", basicCredentials.password) 42 | } 43 | 44 | @Test 45 | fun `test convert with API key credentials`() { 46 | val credentials = Credentials(type = "apikey", apiKey = "sample_api_key") 47 | 48 | val convertedCredentials = credentials.convert() 49 | 50 | // Ensure the converted credentials is of type ApiKeyCredentials 51 | assertTrue(convertedCredentials is ApiKeyCredentials) 52 | assertEquals("sample_api_key", (convertedCredentials as ApiKeyCredentials).apiKey) 53 | } 54 | 55 | @Test 56 | fun `test convert with missing token for bearer credentials`() { 57 | val credentials = Credentials(type = "bearer") 58 | 59 | val exception = assertFailsWith { 60 | credentials.convert() 61 | } 62 | 63 | assertEquals("Token is required for bearer credentials", exception.message) 64 | } 65 | 66 | @Test 67 | fun `test convert with missing username and password for basic credentials`() { 68 | val credentials = Credentials(type = "basic") 69 | 70 | val exception = assertFailsWith { 71 | credentials.convert() 72 | } 73 | 74 | assertEquals("Username and password are required for basic credentials", exception.message) 75 | } 76 | 77 | @Test 78 | fun `test convert with missing apiKey for apiKey credentials`() { 79 | val credentials = Credentials(type = "apikey") 80 | 81 | val exception = assertFailsWith { 82 | credentials.convert() 83 | } 84 | 85 | assertEquals("API Key is required for API Key credentials", exception.message) 86 | } 87 | 88 | @Test 89 | fun `test convert with unknown credential type`() { 90 | val credentials = Credentials(type = "unknown") 91 | 92 | val exception = assertFailsWith { 93 | credentials.convert() 94 | } 95 | 96 | assertEquals("Unknown credentials type: unknown", exception.message) 97 | } 98 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/content/ContentCodec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.content 8 | 9 | import org.eclipse.thingweb.thing.schema.DataSchema 10 | import com.fasterxml.jackson.databind.JsonNode 11 | import kotlin.reflect.KClass 12 | 13 | /** 14 | * A ContentCodec is responsible for (de)serializing data in certain encoding (e.g. JSON, CBOR). 15 | */ 16 | interface ContentCodec { 17 | 18 | /** 19 | * Returns the media type supported by the codec (e.g., `application/json`). 20 | * 21 | * @return The supported media type as a string. 22 | */ 23 | val mediaType: String 24 | 25 | /** 26 | * Deserializes the given `body` according to the data schema defined in `schema`. 27 | * 28 | * This method calls the overloaded version of `bytesToValue`, providing an empty parameters map. 29 | * 30 | * @param body The byte array representing the encoded data. 31 | * @param schema The optional data schema defining the expected structure. 32 | * @return A `JsonNode` representing the deserialized value. 33 | * @throws ContentCodecException If deserialization fails. 34 | */ 35 | fun bytesToValue(body: ByteArray, schema: DataSchema<*>?): JsonNode { 36 | return bytesToValue(body, schema, emptyMap()) 37 | } 38 | 39 | /** 40 | * Deserializes the given `body` according to the data schema defined in `schema`. 41 | * The `parameters` map can contain additional metadata about the encoding, such as 42 | * the character set used. 43 | * 44 | * @param body The byte array representing the encoded data. 45 | * @param schema The optional data schema defining the expected structure. 46 | * @param parameters Additional encoding parameters (e.g., character set). 47 | * @return A `JsonNode` representing the deserialized value. 48 | * @throws ContentCodecException If deserialization fails. 49 | */ 50 | fun bytesToValue( 51 | body: ByteArray, 52 | schema: DataSchema<*>?, 53 | parameters: Map 54 | ): JsonNode 55 | 56 | /** 57 | * Deserializes the given `body` into an instance of type `O`. 58 | * The `parameters` map can contain additional metadata about the encoding. 59 | * 60 | * @param body The byte array representing the encoded data. 61 | * @param parameters Additional encoding parameters (e.g., character set). 62 | * @param The target type of the deserialized value. 63 | * @return The deserialized object of type `O`. 64 | * @throws ContentCodecException If deserialization fails. 65 | */ 66 | fun bytesToValue(body: ByteArray, parameters: Map, clazz: KClass): O 67 | 68 | /** 69 | * Serializes the given `value` according to the provided encoding parameters. 70 | * 71 | * @param value The object to serialize. 72 | * @param parameters Additional encoding parameters (e.g., character set). 73 | * @return A byte array representing the serialized data. 74 | * @throws ContentCodecException If serialization fails. 75 | */ 76 | fun valueToBytes(value: Any, parameters: Map): ByteArray 77 | 78 | /** 79 | * Serializes the given `JsonNode` according to the provided encoding parameters. 80 | * 81 | * @param value The `JsonNode` to serialize. 82 | * @param parameters Additional encoding parameters (e.g., character set). 83 | * @return A byte array representing the serialized JSON data. 84 | * @throws ContentCodecException If serialization fails. 85 | */ 86 | fun valueToBytes(value: JsonNode, parameters: Map): ByteArray 87 | } 88 | -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/Wot.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb 8 | 9 | import org.eclipse.thingweb.security.NoSecurityScheme 10 | import org.eclipse.thingweb.security.SecurityScheme 11 | import org.eclipse.thingweb.thing.ConsumedThing 12 | import org.eclipse.thingweb.thing.ThingDescription 13 | import org.eclipse.thingweb.thing.filter.ThingFilter 14 | import org.eclipse.thingweb.thing.schema.WoTConsumedThing 15 | import org.eclipse.thingweb.thing.schema.WoTExposedThing 16 | import org.eclipse.thingweb.thing.schema.WoTThingDescription 17 | import kotlinx.coroutines.flow.Flow 18 | import java.net.URI 19 | 20 | /** 21 | * Provides methods for discovering, consuming, exposing and fetching things. 22 | * https://w3c.github.io/wot-scripting-api/#the-wot-api-object 23 | */ 24 | interface Wot { 25 | 26 | /** 27 | * Starts the discovery process that will provide Things that match the `filter` 28 | * argument. 29 | * 30 | * @param filter 31 | * @return 32 | */ 33 | fun discover(filter: ThingFilter): Flow 34 | 35 | /** 36 | * Starts the discovery process that will provide all available Things. 37 | * 38 | * @return 39 | */ 40 | fun discover(): Flow 41 | 42 | /** 43 | * Starts the discovery process that will provide Things that match the `filter` 44 | * argument from a given Thing Directory. 45 | * 46 | * @param filter 47 | * @return 48 | */ 49 | suspend fun exploreDirectory(directoryUrl: String, securityScheme: SecurityScheme = NoSecurityScheme()): Set 50 | 51 | /** 52 | * Accepts a `thing` argument of type [ThingDescription] and returns an [WoTExposedThing]. 53 | * 54 | * @param thingDescription 55 | * @return 56 | */ 57 | fun produce(thingDescription: WoTThingDescription): WoTExposedThing 58 | 59 | fun produce(configure: ThingDescription.() -> Unit): WoTExposedThing 60 | 61 | /** 62 | * Accepts a `thing` argument of type [ThingDescription] and returns a [ConsumedThing] object.

63 | * 64 | * The result can be used to interact with a thing. 65 | * 66 | * @param thingDescription 67 | * @return 68 | */ 69 | fun consume(thingDescription: WoTThingDescription): WoTConsumedThing 70 | 71 | /** 72 | * Accepts an [java.net.URL] (e.g. "file:..." or "http://...") to a resource that serves a 73 | * thing description and returns the corresponding Thing object. 74 | * 75 | * @param url 76 | * @return 77 | */ 78 | suspend fun requestThingDescription(url: URI, securityScheme: SecurityScheme = NoSecurityScheme()): WoTThingDescription 79 | 80 | /** 81 | * Accepts an [String] containing an url (e.g. "file:..." or "http://...") to a resource 82 | * that serves a thing description and returns the corresponding Thing object. 83 | * 84 | * @param url 85 | * @return 86 | */ 87 | suspend fun requestThingDescription(url: String, securityScheme : SecurityScheme = NoSecurityScheme()): WoTThingDescription 88 | 89 | companion object { 90 | // Factory method to create an instance of WoT with a given Servient 91 | fun create(servient: Servient): Wot { 92 | return DefaultWot(servient) 93 | } 94 | } 95 | 96 | } 97 | 98 | 99 | open class WotException : RuntimeException { 100 | constructor(message: String?) : super(message) 101 | constructor(cause: Throwable?) : super(cause) 102 | constructor(message: String, cause: Throwable): super(message, cause) 103 | 104 | constructor() : super() 105 | } 106 | 107 | @DslMarker 108 | annotation class WoTDSL -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/SessionAwareProtocolListenerRegistry.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing 8 | 9 | import org.eclipse.thingweb.thing.schema.ContentListener 10 | import org.eclipse.thingweb.thing.schema.DataSchema 11 | import org.eclipse.thingweb.thing.schema.InteractionAffordance 12 | import org.eclipse.thingweb.thing.schema.InteractionInput 13 | import java.util.concurrent.ConcurrentHashMap 14 | import java.util.concurrent.ConcurrentMap 15 | 16 | /** 17 | * A session-aware wrapper for the `ProtocolListenerRegistry`. 18 | * 19 | * This class extends the functionality of `ProtocolListenerRegistry` by providing a simplified 20 | * interface for managing listeners across multiple sessions. 21 | * 22 | * Internally, it uses an instance of `ProtocolListenerRegistry` to delegate the management 23 | * of listeners and affordances. 24 | */ 25 | class SessionAwareProtocolListenerRegistry { 26 | 27 | internal val registryMap: ConcurrentMap = ConcurrentHashMap() 28 | 29 | /** 30 | * Registers a listener for a specific session, affordance, and form index. 31 | * 32 | * @param sessionId The unique ID of the session. 33 | * @param affordance The interaction affordance for which the listener is being registered. 34 | * @param formIndex The index of the form associated with the affordance. 35 | * @param listener The listener to register. 36 | * 37 | * Delegates to `ProtocolListenerRegistry.register`. 38 | */ 39 | fun register(sessionId: String, affordance: InteractionAffordance, formIndex: Int, listener: ContentListener) { 40 | val registry = registryMap.getOrPut(sessionId) { ProtocolListenerRegistry() } 41 | registry.register(affordance, formIndex, listener) 42 | } 43 | 44 | /** 45 | * Unregisters a listener for a specific session, affordance, and form index. 46 | * 47 | * @param sessionId The unique ID of the session. 48 | * @param affordance The interaction affordance from which the listener is being removed. 49 | * @param formIndex The index of the form associated with the affordance. 50 | * 51 | * Delegates to `ProtocolListenerRegistry.unregister`. 52 | */ 53 | fun unregister(sessionId: String, affordance: InteractionAffordance, formIndex: Int) { 54 | val registry = registryMap[sessionId] ?: throw IllegalStateException("Session not found") 55 | registry.unregister(affordance, formIndex) 56 | } 57 | 58 | /** 59 | * Unregisters all listeners associated with a specific session. 60 | * 61 | * @param sessionId The unique ID of the session. 62 | */ 63 | fun unregisterAll(sessionId: String) { 64 | registryMap.remove(sessionId) 65 | } 66 | 67 | /** 68 | * Unregisters all listeners across all sessions. 69 | * 70 | */ 71 | fun unregisterAllSessions() { 72 | registryMap.clear() 73 | } 74 | 75 | /** 76 | * Notifies all listeners across all sessions for a given affordance and optional form index. 77 | * 78 | * @param affordance The interaction affordance for which listeners are to be notified. 79 | * @param data The input data to pass to the listeners. 80 | * @param schema The schema to validate or transform the input data (optional). 81 | * @param formIndex The index of the form to notify listeners for (optional). 82 | * 83 | * Delegates to `ProtocolListenerRegistry.notify`. 84 | */ 85 | suspend fun notify( 86 | affordance: InteractionAffordance, 87 | data: InteractionInput, 88 | schema: DataSchema? = null, 89 | formIndex: Int? = null 90 | ) { 91 | registryMap.values.forEach { registry -> 92 | registry.notify(affordance, data, schema, formIndex) 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /kotlin-wot/src/test/kotlin/thing/credentials/DefaultCredentialsProviderTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing.credentials 8 | 9 | 10 | import org.eclipse.thingweb.credentials.BasicCredentials 11 | import org.eclipse.thingweb.credentials.BearerCredentials 12 | import org.eclipse.thingweb.credentials.DefaultCredentialsProvider 13 | import org.eclipse.thingweb.credentials.NoCredentialsFound 14 | import org.eclipse.thingweb.security.BasicSecurityScheme 15 | import org.eclipse.thingweb.security.BearerSecurityScheme 16 | import org.eclipse.thingweb.security.SecurityScheme 17 | import org.eclipse.thingweb.thing.form.Form 18 | import kotlin.test.* 19 | 20 | class DefaultCredentialsProviderTest { 21 | 22 | @Test 23 | fun `should return BasicCredentials when href matches key`() { 24 | val securitySchemes = listOf(BasicSecurityScheme()) 25 | val credentials = mapOf( 26 | "https://example.com/device1" to BasicCredentials("user1", "pass1") 27 | ) 28 | val provider = DefaultCredentialsProvider(securitySchemes, credentials) 29 | val form = Form(href = "https://example.com/device1/status") 30 | 31 | val result = provider.getCredentials(form) 32 | 33 | assertTrue(result is BasicCredentials) 34 | assertEquals("user1", result.username) 35 | assertEquals("pass1", result.password) 36 | } 37 | 38 | @Test 39 | fun `should return BearerCredentials when href matches key`() { 40 | val securitySchemes = listOf(BearerSecurityScheme()) 41 | val credentials = mapOf( 42 | "https://secure.com/api" to BearerCredentials("secureToken123") 43 | ) 44 | val provider = DefaultCredentialsProvider(securitySchemes, credentials) 45 | val form = Form(href = "https://secure.com/api/resource") 46 | 47 | val result = provider.getCredentials(form) 48 | 49 | assertTrue(result is BearerCredentials) 50 | assertEquals("secureToken123", result.token) 51 | } 52 | 53 | @Test 54 | fun `should throw exception when no matching credentials found`() { 55 | val securitySchemes = listOf(BasicSecurityScheme()) 56 | val credentials = mapOf( 57 | "https://example.com/device1" to BasicCredentials("user1", "pass1") 58 | ) 59 | val provider = DefaultCredentialsProvider(securitySchemes, credentials) 60 | val form = Form(href = "https://unknown.com/deviceX") 61 | 62 | assertFailsWith { provider.getCredentials(form) } 63 | } 64 | 65 | @Test 66 | fun `should return null when securitySchemes is empty`() { 67 | val securitySchemes = emptyList() 68 | val credentials = mapOf( 69 | "https://example.com/device1" to BasicCredentials("user1", "pass1") 70 | ) 71 | val provider = DefaultCredentialsProvider(securitySchemes, credentials) 72 | val form = Form(href = "https://example.com/device1/status") 73 | 74 | val result = provider.getCredentials(form) 75 | 76 | assertNull(result) 77 | } 78 | 79 | @Test 80 | fun `should throw exception for mismatched credential type`() { 81 | val securitySchemes = listOf(BearerSecurityScheme()) // Expected BearerCredentials 82 | val credentials = mapOf( 83 | "https://example.com/device1" to BasicCredentials("user1", "pass1") // Providing BasicCredentials 84 | ) 85 | val provider = DefaultCredentialsProvider(securitySchemes, credentials) 86 | val form = Form(href = "https://example.com/device1/status") 87 | 88 | val exception = assertFailsWith { 89 | provider.getCredentials(form) 90 | } 91 | assertTrue(exception.message!!.contains("Expected BearerCredentials but found BasicCredentials")) 92 | } 93 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/ContextSerializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing 8 | 9 | import org.eclipse.thingweb.thing.schema.Context 10 | import com.fasterxml.jackson.core.JsonGenerator 11 | import com.fasterxml.jackson.databind.JsonSerializer 12 | import com.fasterxml.jackson.databind.SerializerProvider 13 | import java.io.IOException 14 | 15 | /** 16 | * Serializes the single context or the list of contexts of a [ThingDescription] to JSON. Is used by 17 | * Jackson 18 | */ 19 | internal class ContextSerializer : JsonSerializer() { 20 | @Throws(IOException::class) 21 | override fun serialize( 22 | context: Context, 23 | gen: JsonGenerator, 24 | serializers: SerializerProvider 25 | ) { 26 | val defaultUrls: List = context.defaultUrls 27 | val prefixedUrls: Map = context.prefixedUrls 28 | val hasMoreThanOneDefaultUrl = defaultUrls.size > 1 29 | val hasPrefixedUrls = prefixedUrls.isNotEmpty() 30 | if (hasMoreThanOneDefaultUrl || hasPrefixedUrls) { 31 | gen.writeStartArray() 32 | } 33 | defaultUrls.forEach { 34 | gen.writeString(it) 35 | } 36 | if (hasPrefixedUrls) { 37 | gen.writeObject(prefixedUrls) 38 | } 39 | if (hasMoreThanOneDefaultUrl || hasPrefixedUrls) { 40 | gen.writeEndArray() 41 | } 42 | } 43 | } 44 | 45 | /* 46 | internal object ContextSerializer : KSerializer { 47 | override val descriptor = buildClassSerialDescriptor("Context") 48 | private val delegateSerializer = MapSerializer(String.serializer(), String.serializer()) 49 | override fun serialize(encoder: Encoder, value: Context) { 50 | 51 | val hasDefaultUrl = value.defaultUrl != null 52 | val hasPrefixedUrls = value.prefixedUrls.isNotEmpty() 53 | 54 | // Prepare to encode the structure 55 | if (hasDefaultUrl && hasPrefixedUrls) { 56 | // Both present: serialize as an array 57 | encoder.encodeStructure(descriptor) { 58 | // Encode the default URL 59 | encoder.encodeString(value.defaultUrl!!) 60 | // Encode the prefixed URLs 61 | encoder.encodeSerializableValue(delegateSerializer, value.prefixedUrls) 62 | } 63 | } else if (hasDefaultUrl) { 64 | // Only serialize the default URL as a plain string 65 | encoder.encodeString(value.defaultUrl!!) 66 | } else if (hasPrefixedUrls) { 67 | // Serialize only the prefixed URLs as an object 68 | encoder.encodeSerializableValue(delegateSerializer, value.prefixedUrls) 69 | } 70 | } 71 | 72 | override fun deserialize(decoder: Decoder): Context { 73 | val compositeInput = decoder.beginStructure(descriptor) 74 | var defaultUrl: String? = null 75 | val prefixedUrls = mutableMapOf() 76 | 77 | // Read elements from the structure 78 | loop@ while (true) { 79 | when (val index = compositeInput.decodeElementIndex(descriptor)) { 80 | CompositeDecoder.DECODE_DONE -> break@loop 81 | 0 -> defaultUrl = compositeInput.decodeStringElement(descriptor, index) 82 | 1 -> { 83 | // Deserialize the prefixedUrls map 84 | prefixedUrls.putAll(compositeInput.decodeSerializableElement(descriptor, index, MapSerializer(String.serializer(), String.serializer()))) 85 | } 86 | else -> throw SerializationException("Unknown index: $index") 87 | } 88 | } 89 | 90 | compositeInput.endStructure(descriptor) 91 | 92 | val context = Context() 93 | defaultUrl?.let { context.addContext(it) } 94 | prefixedUrls.forEach { (prefix, url) -> context.addContext(prefix, url) } 95 | 96 | return context 97 | } 98 | } 99 | */ 100 | -------------------------------------------------------------------------------- /kotlin-wot/src/test/kotlin/content/JsonCodecText.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.content 8 | 9 | import org.eclipse.thingweb.thing.schema.BooleanSchema 10 | import org.eclipse.thingweb.thing.schema.IntegerSchema 11 | import org.eclipse.thingweb.thing.schema.ObjectSchema 12 | import org.eclipse.thingweb.thing.schema.StringSchema 13 | import com.fasterxml.jackson.databind.node.BooleanNode 14 | import com.fasterxml.jackson.databind.node.IntNode 15 | import com.fasterxml.jackson.databind.node.LongNode 16 | import com.fasterxml.jackson.databind.node.TextNode 17 | import org.junit.jupiter.api.Assertions.* 18 | import org.junit.jupiter.api.BeforeEach 19 | import org.junit.jupiter.api.Test 20 | import org.junit.jupiter.api.assertThrows 21 | 22 | class JsonCodecTest { 23 | 24 | private lateinit var jsonCodec: JsonCodec 25 | 26 | @BeforeEach 27 | fun setup() { 28 | jsonCodec = JsonCodec() 29 | } 30 | 31 | @Test 32 | fun `mediaType should return application json`() { 33 | assertEquals("application/json", jsonCodec.mediaType) 34 | } 35 | 36 | @Test 37 | fun `bytesToValue should decode JSON byte array to string successfully`() { 38 | 39 | // Act 40 | val result = jsonCodec.bytesToValue(""""test"""".toByteArray(), StringSchema(), emptyMap()) 41 | 42 | // Assert 43 | assertEquals("test", result.textValue()) 44 | } 45 | 46 | @Test 47 | fun `bytesToValue should decode JSON byte array to boolean successfully`() { 48 | 49 | // Act 50 | val result = jsonCodec.bytesToValue("""true""".toByteArray(), BooleanSchema(), emptyMap()) 51 | 52 | // Assert 53 | assertEquals(true, result.booleanValue()) 54 | } 55 | 56 | @Test 57 | fun `bytesToValue should decode JSON byte array to integer successfully`() { 58 | 59 | // Act 60 | val result = jsonCodec.bytesToValue("""1""".toByteArray(), IntegerSchema(), emptyMap()) 61 | 62 | // Assert 63 | assertEquals(1, result.intValue()) 64 | } 65 | 66 | 67 | @Test 68 | fun `bytesToValue should throw ContentCodecException on invalid JSON`() { 69 | // Arrange 70 | val invalidJsonBytes = """{"key": "value"""".toByteArray() // Missing closing brace 71 | 72 | // Act & Assert 73 | val exception = assertThrows { 74 | jsonCodec.bytesToValue(invalidJsonBytes, ObjectSchema(), emptyMap()) 75 | } 76 | assertTrue(exception.message!!.contains("Failed to decode")) 77 | } 78 | 79 | /* 80 | @Test 81 | fun `valueToBytes should encode json object to map successfully`() { 82 | val json = """{"key": "value"}""".toByteArray() 83 | // Act 84 | val result = jsonCodec.bytesToValue(json, ObjectSchema(), emptyMap()) 85 | 86 | // Assert 87 | assertEquals("value", result.value["key"]) 88 | } 89 | 90 | */ 91 | 92 | @Test 93 | fun `valueToBytes should encode StringValue to JSON byte array`() { 94 | val value = TextNode("test") 95 | val result = jsonCodec.valueToBytes(value, emptyMap()) 96 | assertArrayEquals(""""test"""".toByteArray(), result) 97 | } 98 | 99 | @Test 100 | fun `valueToBytes should encode BooleanValue to JSON byte array`() { 101 | val value = BooleanNode.TRUE 102 | val result = jsonCodec.valueToBytes(value, emptyMap()) 103 | assertArrayEquals("true".toByteArray(), result) 104 | } 105 | 106 | @Test 107 | fun `valueToBytes should encode ArrayValue to JSON byte array`() { 108 | val result = jsonCodec.valueToBytes(listOf("test", 1), emptyMap()) 109 | assertArrayEquals("""["test",1]""".toByteArray(), result) 110 | } 111 | 112 | @Test 113 | fun `valueToBytes should encode ObjectValue to JSON byte array`() { 114 | val result = jsonCodec.valueToBytes(mapOf("bla" to "blub"), emptyMap()) 115 | 116 | assertArrayEquals("""{"bla":"blub"}""".toByteArray(), result) 117 | } 118 | } -------------------------------------------------------------------------------- /kotlin-wot-reflection/src/test/kotlin/reflection/BuildObjectSchemaTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.reflection 8 | 9 | import org.eclipse.thingweb.reflection.ExposedThingBuilder.buildObjectSchema 10 | import org.eclipse.thingweb.thing.schema.DataSchema 11 | import org.eclipse.thingweb.thing.schema.IntegerSchema 12 | import org.eclipse.thingweb.thing.schema.StringSchema 13 | import kotlin.test.Test 14 | import kotlin.test.assertEquals 15 | 16 | class BuildObjectSchemaTest { 17 | 18 | // Function with non-nullable parameters (MyClass) 19 | fun functionWithRequiredParameters(id: Int, name: String) {} 20 | 21 | // Function with nullable parameters (MyClassWithOptional) 22 | fun functionWithNullableParameter(id: Int, name: String?) {} 23 | 24 | // Function with a mix of nullable and non-nullable parameters (MyClassMixed) 25 | fun functionWithMixedParameters(id: Int, name: String?, age: Int = 30) {} 26 | 27 | // Function with a parameter having a default value (optional) 28 | fun functionWithOptionalParameter(id: Int, name: String = "Guest", age: Int = 30) {} 29 | 30 | // Test case 1: Non-nullable parameters (myClass) 31 | @Test 32 | fun `test buildObjectSchema with non-nullable parameters`() { 33 | val parameterTypes = ::functionWithRequiredParameters.parameters 34 | 35 | val schema = buildObjectSchema(parameterTypes) 36 | 37 | val expectedProperties: Map> = mapOf( 38 | "id" to IntegerSchema(), 39 | "name" to StringSchema() 40 | ) 41 | val expectedRequired = listOf("id", "name") 42 | 43 | assertEquals(expectedProperties, schema.properties) 44 | assertEquals(expectedRequired, schema.required) 45 | } 46 | 47 | // Test case 2: Nullable parameters (myClassWithOptional) 48 | @Test 49 | fun `test buildObjectSchema with nullable parameters`() { 50 | val parameterTypes = ::functionWithNullableParameter.parameters 51 | 52 | val schema = buildObjectSchema(parameterTypes) 53 | 54 | val expectedProperties: Map> = mapOf( 55 | "id" to IntegerSchema(), 56 | "name" to StringSchema() // nullable but included as a property 57 | ) 58 | val expectedRequired = listOf("id") // name is nullable, so not required 59 | 60 | assertEquals(expectedProperties, schema.properties) 61 | assertEquals(expectedRequired, schema.required) 62 | } 63 | 64 | // Test case 3: Mix of nullable and non-nullable parameters (myClassMixed) 65 | @Test 66 | fun `test buildObjectSchema with a mix of nullable and non-nullable parameters`() { 67 | val parameterTypes = ::functionWithMixedParameters.parameters 68 | 69 | val schema = buildObjectSchema(parameterTypes) 70 | 71 | val expectedProperties: Map> = mapOf( 72 | "id" to IntegerSchema(), 73 | "name" to StringSchema(), 74 | "age" to IntegerSchema() 75 | ) 76 | val expectedRequired = listOf("id") // name is nullable, so not required 77 | 78 | assertEquals(expectedProperties, schema.properties) 79 | assertEquals(expectedRequired, schema.required) 80 | } 81 | 82 | // Test case 4: Function with optional parameters (greet) 83 | @Test 84 | fun `test optional parameter`() { 85 | // Get the parameters of the 'greet' function using reflection 86 | val parameterTypes = ::functionWithOptionalParameter.parameters 87 | 88 | val schema = buildObjectSchema(parameterTypes) 89 | 90 | val expectedProperties: Map> = mapOf( 91 | "id" to IntegerSchema(), 92 | "name" to StringSchema(), 93 | "age" to IntegerSchema() 94 | ) 95 | val expectedRequired = listOf("id") // name is nullable, so not required 96 | 97 | assertEquals(expectedProperties, schema.properties) 98 | assertEquals(expectedRequired, schema.required) 99 | } 100 | } -------------------------------------------------------------------------------- /kotlin-wot-binding-http/src/test/kotlin/http/HttpProtocolClientTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.binding.http 8 | import org.eclipse.thingweb.content.Content 9 | import org.eclipse.thingweb.security.BasicSecurityScheme 10 | import org.eclipse.thingweb.security.BearerSecurityScheme 11 | import org.eclipse.thingweb.security.NoSecurityScheme 12 | import org.eclipse.thingweb.security.SecurityScheme 13 | import org.eclipse.thingweb.thing.form.Form 14 | import ai.anfc.lmos.wot.binding.ProtocolClientException 15 | import com.github.tomakehurst.wiremock.WireMockServer 16 | import com.marcinziolo.kotlin.wiremock.* 17 | import io.mockk.every 18 | import io.mockk.mockk 19 | import kotlinx.coroutines.test.runTest 20 | import org.junit.jupiter.api.AfterEach 21 | import kotlin.test.BeforeTest 22 | import kotlin.test.Test 23 | import kotlin.test.assertFailsWith 24 | 25 | class HttpProtocolClientTest { 26 | 27 | private lateinit var form: Form 28 | private lateinit var securityScheme: SecurityScheme 29 | 30 | private val wiremock: WireMockServer = WireMockServer(8080) 31 | 32 | @BeforeTest 33 | fun setUp() { 34 | wiremock.start() 35 | form = mockk() 36 | securityScheme = mockk() 37 | } 38 | 39 | @AfterEach 40 | fun afterEach() { 41 | wiremock.resetAll() 42 | wiremock.stop() 43 | } 44 | 45 | @Test 46 | fun readResourceCreatesProperRequest() = runTest { 47 | wiremock.get { 48 | url equalTo "/foo" 49 | } returns { 50 | statusCode = 200 51 | } 52 | 53 | val form = Form("${wiremock.baseUrl()}/foo") 54 | 55 | val client = HttpProtocolClient() 56 | client.readResource(form) 57 | 58 | wiremock.verify { 59 | url equalTo "/foo" 60 | exactly = 1 61 | } 62 | } 63 | 64 | @Test 65 | fun writeResourceCreatesProperRequest() = runTest { 66 | wiremock.put { 67 | url equalTo "/foo" 68 | body equalTo """{"key": "value"}""" 69 | } returns { 70 | statusCode = 200 71 | } 72 | 73 | val form = Form("${wiremock.baseUrl()}/foo") 74 | val jsonContent = """{"key": "value"}""" 75 | val content = Content("application/json", jsonContent.toByteArray()) 76 | 77 | val client = HttpProtocolClient() 78 | client.writeResource(form, content) 79 | 80 | wiremock.verify { 81 | url equalTo "/foo" 82 | exactly = 1 83 | } 84 | } 85 | 86 | @Test 87 | fun invokeResourceCreatesProperRequest() = runTest { 88 | wiremock.post { 89 | url equalTo "/foo" 90 | body equalTo """{"key": "value"}""" 91 | } returns { 92 | statusCode = 200 93 | } 94 | 95 | val form = Form("${wiremock.baseUrl()}/foo") 96 | val jsonContent = """{"key": "value"}""" 97 | val content = Content("application/json", jsonContent.toByteArray()) 98 | 99 | val client = HttpProtocolClient() 100 | client.invokeResource(form, content) 101 | 102 | wiremock.verify { 103 | url equalTo "/foo" 104 | exactly = 1 105 | } 106 | } 107 | 108 | @Test 109 | fun subscribeResourceHandlesLongPolling() = runTest { 110 | wiremock.get { 111 | url equalTo "/foo" 112 | } returns { 113 | statusCode = 200 114 | } 115 | 116 | every { form.href } returns "${wiremock.baseUrl()}/foo" 117 | 118 | val client = HttpProtocolClient() 119 | client.subscribeResource(form) 120 | 121 | } 122 | 123 | @Test 124 | fun resolveRequestToContentThrowsExceptionForInvalidResponse() = runTest { 125 | wiremock.get { 126 | url equalTo "/foo" 127 | } returns { 128 | statusCode = 500 129 | } 130 | 131 | every { form.href } returns "${wiremock.baseUrl()}/foo" 132 | 133 | val client = HttpProtocolClient() 134 | 135 | assertFailsWith { 136 | client.readResource(form) 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/ProtocolHelpers.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing 8 | 9 | import org.eclipse.thingweb.thing.form.Form 10 | import org.eclipse.thingweb.thing.form.Operation 11 | import org.eclipse.thingweb.thing.schema.InteractionAffordance 12 | import org.eclipse.thingweb.thing.schema.PropertyAffordance 13 | import java.net.URI 14 | 15 | // Find a form matching the URI scheme, request URL, and optionally content type 16 | 17 | fun findRequestMatchingFormIndex( 18 | forms: List?, 19 | uriScheme: String, 20 | requestUrl: String?, 21 | contentType: String? = null 22 | ): Int { 23 | if (forms == null) return 0 24 | 25 | // First, find forms with matching URL protocol and path 26 | var matchingForms: List = forms.filter { form -> 27 | // Remove optional URI variables from href 28 | val formUrl = URI(form.href.replace(Regex("\\{[\\S]*\\}"), "")) 29 | 30 | // Remove URI variables from the request URL, if any 31 | val reqUrl = requestUrl?.takeIf { it.contains("?").not() } 32 | 33 | formUrl.scheme == uriScheme && (reqUrl == null || formUrl.path == reqUrl) 34 | } 35 | 36 | // Optionally try to match form's content type to the request's one 37 | if (contentType != null) { 38 | matchingForms = matchingForms.filter { form -> form.contentType == contentType } 39 | } 40 | 41 | return if (matchingForms.isNotEmpty()) forms.indexOf(matchingForms[0]) else 0 42 | } 43 | 44 | // Get the form index for a particular operation 45 | 46 | 47 | fun getFormIndexForOperation( 48 | interaction: InteractionAffordance, 49 | type: String, 50 | operation: Operation? = null, 51 | formIndex: Int? = null 52 | ): Int { 53 | var finalFormIndex = -1 54 | 55 | // Default operations based on the type 56 | val defaultOps = mutableListOf() 57 | when (type) { 58 | "property" -> { 59 | interaction as PropertyAffordance<*> 60 | if (interaction.readOnly && operation == Operation.WRITE_PROPERTY || interaction.writeOnly && operation == Operation.READ_PROPERTY) return finalFormIndex 61 | 62 | if (!interaction.readOnly) defaultOps.add(Operation.WRITE_PROPERTY) 63 | if (!interaction.writeOnly) defaultOps.add(Operation.READ_PROPERTY) 64 | } 65 | "action" -> defaultOps.add(Operation.INVOKE_ACTION) 66 | "event" -> defaultOps.addAll(listOf(Operation.SUBSCRIBE_EVENT, Operation.UNSUBSCRIBE_EVENT)) 67 | } 68 | 69 | // If a form index is given, check if it's valid 70 | if (formIndex != null && interaction.forms.size > formIndex) { 71 | val form = interaction.forms[formIndex] 72 | if (operation == null || form.op?.contains(operation) == true) { 73 | finalFormIndex = formIndex 74 | } 75 | } 76 | 77 | // Loop through all forms if no form was found yet 78 | if (finalFormIndex == -1) { 79 | if (operation != null) { 80 | interaction.forms.forEachIndexed { index, form -> 81 | if (form.op?.contains(operation) == true) { 82 | finalFormIndex = index 83 | return@forEachIndexed 84 | } 85 | } 86 | } else { 87 | interaction.forms.forEachIndexed { index, _ -> 88 | finalFormIndex = index 89 | return@forEachIndexed 90 | } 91 | } 92 | } 93 | 94 | // Return the final form index 95 | return finalFormIndex 96 | } 97 | 98 | // Get possible operation values for a property 99 | fun getPropertyOpValues(property: PropertyAffordance<*>): List { 100 | val op = mutableListOf() 101 | 102 | if (!property.readOnly) { 103 | op.add(Operation.WRITE_PROPERTY) 104 | } 105 | 106 | if (!property.writeOnly) { 107 | op.add(Operation.READ_PROPERTY) 108 | } 109 | 110 | if (op.isEmpty()) { 111 | println("Warning: Property was declared both as readOnly and writeOnly.") 112 | } 113 | 114 | if (property.observable) { 115 | op.add(Operation.OBSERVE_PROPERTY) 116 | op.add(Operation.UNOBSERVE_PROPERTY) 117 | } 118 | return op 119 | } 120 | -------------------------------------------------------------------------------- /kotlin-wot-reflection/src/test/kotlin/reflection/AddHandlerTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.reflection 8 | 9 | import org.eclipse.thingweb.reflection.ExposedThingBuilder.addActionHandler 10 | import org.eclipse.thingweb.reflection.ExposedThingBuilder.addEventHandler 11 | import org.eclipse.thingweb.reflection.ExposedThingBuilder.addPropertyHandler 12 | import org.eclipse.thingweb.thing.ExposedThing 13 | import io.mockk.every 14 | import io.mockk.mockk 15 | import io.mockk.verify 16 | import kotlinx.coroutines.test.runTest 17 | import kotlin.reflect.* 18 | import kotlin.test.Test 19 | 20 | class AddHandlerTest { 21 | 22 | data class MyClass(val name: String) 23 | 24 | @Test 25 | fun `test addEventHandler should handle events correctly`() = runTest { 26 | // Mocking dependencies 27 | val exposedThing = mockk(relaxed = true) 28 | val eventFunction = mockk>() 29 | val instance = mockk(relaxed = true) 30 | 31 | // Calling the method 32 | val eventMap = mutableMapOf("eventName" to eventFunction) 33 | addEventHandler(eventMap, exposedThing, instance) 34 | 35 | // Verifying that event subscribe handler was set 36 | verify { exposedThing.setEventSubscribeHandler("eventName", any()) } 37 | } 38 | 39 | @Test 40 | fun `test addActionHandler should handle actions correctly`() { 41 | // Mocking dependencies 42 | val exposedThing = mockk(relaxed = true) 43 | val actionFunction = mockk>() 44 | val instance = mockk(relaxed = true) 45 | 46 | // Calling the method 47 | val actionMap = mutableMapOf("actionName" to actionFunction) 48 | addActionHandler(actionMap, exposedThing, instance) 49 | 50 | // Verifying that action handler was set 51 | verify { exposedThing.setActionHandler("actionName", any()) } 52 | } 53 | 54 | @Test 55 | fun `test addPropertyHandler should handle read-only properties`() { 56 | // Mocking dependencies 57 | val exposedThing = mockk(relaxed = true) 58 | val readOnlyProperty = mockk>() 59 | val instance = MyClass("test") 60 | val returnType = mockk() 61 | val classifier = mockk() 62 | 63 | // Mocking getter for property 64 | every { readOnlyProperty.returnType } returns returnType 65 | every { returnType.classifier } returns classifier 66 | every { readOnlyProperty.getter.call(instance) } returns "testValue" 67 | 68 | // Maps for read-only, write-only, and read-write properties 69 | val readOnlyPropertiesMap = mutableMapOf("readOnlyProperty" to readOnlyProperty) 70 | val writeOnlyPropertiesMap = mutableMapOf>() 71 | val readWritePropertiesMap = mutableMapOf>() 72 | 73 | // Calling the method for read-only properties 74 | addPropertyHandler(readOnlyPropertiesMap, exposedThing, instance, writeOnlyPropertiesMap, readWritePropertiesMap) 75 | 76 | // Verifying that property read handler was set 77 | verify { exposedThing.setPropertyReadHandler("readOnlyProperty", any()) } 78 | 79 | } 80 | 81 | @Test 82 | fun `test addPropertyHandler should handle write-only properties`() { 83 | // Mocking dependencies 84 | val exposedThing = mockk(relaxed = true) 85 | val writeOnlyProperty = mockk>() 86 | val instance = MyClass("test") 87 | 88 | // Mocking setter for write-only property 89 | val setter : KMutableProperty1.Setter = mockk() 90 | every { writeOnlyProperty.setter } returns setter 91 | 92 | // Maps for read-only, write-only, and read-write properties 93 | val readOnlyPropertiesMap = mutableMapOf>() 94 | val writeOnlyPropertiesMap = mutableMapOf("writeOnlyProperty" to writeOnlyProperty) 95 | val readWritePropertiesMap = mutableMapOf>() 96 | 97 | // Calling the method for write-only properties 98 | addPropertyHandler(readOnlyPropertiesMap, exposedThing, instance, writeOnlyPropertiesMap, readWritePropertiesMap) 99 | 100 | // Verifying that property write handler was set 101 | verify { exposedThing.setPropertyWriteHandler("writeOnlyProperty", any()) } 102 | 103 | } 104 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release-type: 7 | type: choice 8 | description: What do you want to release? 9 | options: 10 | - Milestone 11 | - Release 12 | workflow_call: 13 | inputs: 14 | release-type: 15 | default: "Milestone" 16 | required: true 17 | type: string 18 | registry-name: 19 | default: eclipse-lmos 20 | type: string 21 | required: false 22 | secrets: 23 | oss-username: 24 | required: true 25 | oss-password: 26 | required: true 27 | signing-key-id: 28 | required: true 29 | signing-key: 30 | required: true 31 | signing-key-password: 32 | required: true 33 | bot-token: 34 | required: true 35 | registry-username: 36 | required: false 37 | registry-password: 38 | required: false 39 | outputs: 40 | version: 41 | value: ${{ jobs.build-and-release.outputs.version }} 42 | 43 | jobs: 44 | build-and-release: 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: write 48 | packages: write 49 | outputs: 50 | version: ${{ steps.Publish.outputs.version }} 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v4.2.2 54 | with: 55 | ref: master 56 | token: ${{ secrets.bot-token }} 57 | - name: REUSE Compliance Check 58 | uses: fsfe/reuse-action@v5.0.0 59 | - name: Set up JDK 21 60 | uses: actions/setup-java@v4.7.0 61 | with: 62 | java-version: '21' 63 | distribution: 'temurin' 64 | - name: Setup Gradle 65 | uses: gradle/actions/setup-gradle@v4.3.0 66 | - name: Publish 67 | id: Publish 68 | env: 69 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.oss-username }} 70 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.oss-password }} 71 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.signing-key-id }} 72 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.signing-key }} 73 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.signing-key-password }} 74 | REGISTRY_URL: ghcr.io 75 | REGISTRY_USERNAME: ${{ secrets.registry-username }} 76 | REGISTRY_PASSWORD: ${{ secrets.registry-password }} 77 | REGISTRY_NAMESPACE: ${{ inputs.registry-name }} 78 | GH_TOKEN: ${{ secrets.bot-token }} 79 | run: | 80 | git config --global user.email "cicd@ancf.ai" 81 | git config --global user.name "CICD" 82 | git fetch -t -q 83 | 84 | # Extract version without -SNAPSHOT suffix from gradle.properties 85 | version=$(sed -n -E 's/^version[[:blank:]]*=[[:blank:]]*(.*)-SNAPSHOT$/\1/p' gradle.properties) 86 | oldSnapshotVersion="${version}-SNAPSHOT" 87 | 88 | # In case of milestone release, set milestone release version based on previous milestone release versions 89 | if [ "${{ github.event.inputs.release-type }}" == "Milestone" ]; then 90 | oldMilestone=$(git tag -l "${version}-M*" --sort=v:refname | tail -n 1) 91 | if [ "${oldMilestone}" == "" ]; then 92 | version=${version}-M1 93 | else 94 | version=${version}-M$((10#${oldMilestone##*-M}+1)) 95 | fi 96 | fi 97 | 98 | # In case of non-milestone releses, increase the snapshot version in master branch 99 | if [ "${{ github.event.inputs.release-type }}" == "Milestone" ]; then 100 | nextSnapshotVersion="${oldSnapshotVersion}" 101 | else 102 | major=$(echo $version | cut -d. -f1) 103 | minor=$(echo $version | cut -d. -f2) 104 | nextSnapshotVersion="${major}.$((minor+1)).0-SNAPSHOT" 105 | fi 106 | 107 | echo "Releasing ${version}" 108 | echo "version=${version}" >> $GITHUB_OUTPUT 109 | 110 | # Update version, build, publish release jars and (optionally) docker image and helm chart 111 | ./gradlew :release -Prelease.useAutomaticVersion=true "-Prelease.releaseVersion=${version}" "-Prelease.newVersion=${nextSnapshotVersion}" --no-parallel 112 | git checkout "${version}" 113 | ./gradlew publish --no-configuration-cache 114 | 115 | # Generate release notes 116 | if [ "${{ github.event.inputs.release-type }}" == "Milestone" ]; then 117 | gh release create "${version}" --generate-notes --prerelease 118 | else 119 | gh release create "${version}" --generate-notes --latest 120 | fi 121 | -------------------------------------------------------------------------------- /kotlin-wot/src/test/kotlin/thing/SessionAwareProtocolListenerRegistryTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing 8 | 9 | import org.eclipse.thingweb.thing.form.Form 10 | import org.eclipse.thingweb.thing.schema.ContentListener 11 | import org.eclipse.thingweb.thing.schema.StringProperty 12 | import org.eclipse.thingweb.thing.schema.toInteractionInputValue 13 | import io.mockk.coEvery 14 | import io.mockk.mockk 15 | import kotlinx.coroutines.test.runTest 16 | import org.junit.jupiter.api.Assertions.assertTrue 17 | import kotlin.test.Test 18 | import kotlin.test.assertNotNull 19 | import kotlin.test.assertNull 20 | 21 | class SessionAwareProtocolListenerRegistryTest { 22 | 23 | @Test 24 | fun registerAddsListenerForSpecificSessionSuccessfully() { 25 | val registry = SessionAwareProtocolListenerRegistry() 26 | val affordance = StringProperty(forms = mutableListOf(Form(href = "http://example.com", contentType = "application/json"))) 27 | val listener = ContentListener { content -> println(content) } 28 | 29 | registry.register("session1", affordance, 0, listener) 30 | assertNotNull( 31 | registry.registryMap["session1"]?.listeners?.get(affordance)?.get(0) 32 | ) 33 | } 34 | 35 | @Test 36 | fun notifyCallsListenersAcrossAllSessions() = runTest { 37 | val registry = SessionAwareProtocolListenerRegistry() 38 | val affordance = StringProperty(forms = mutableListOf(Form(href = "http://example.com", contentType = "application/json"))) 39 | val listener1 = mockk(relaxed = true) 40 | val listener2 = mockk(relaxed = true) 41 | 42 | registry.register("session1", affordance, 0, listener1) 43 | registry.register("session2", affordance, 0, listener2) 44 | registry.notify(affordance, "testContent".toInteractionInputValue(), affordance, 0) 45 | 46 | coEvery { listener1.handle(any()) } 47 | coEvery { listener2.handle(any()) } 48 | } 49 | 50 | @Test 51 | fun unregisterListenerForSpecificSession() { 52 | val registry = SessionAwareProtocolListenerRegistry() 53 | val affordance = StringProperty(forms = mutableListOf(Form(href = "http://example.com", contentType = "application/json"))) 54 | val listener = ContentListener { content -> println(content) } 55 | 56 | registry.register("session1", affordance, 0, listener) 57 | registry.unregister("session1", affordance, 0) 58 | assertNull( 59 | registry.registryMap["session1"]?.listeners?.get(affordance)?.get(0) 60 | ) 61 | } 62 | 63 | @Test 64 | fun unregisterAllListenersForSpecificSession() { 65 | val registry = SessionAwareProtocolListenerRegistry() 66 | val affordance = StringProperty(forms = mutableListOf(Form(href = "http://example.com", contentType = "application/json"))) 67 | val listener1 = ContentListener { content -> println(content) } 68 | val listener2 = ContentListener { content -> println(content) } 69 | 70 | registry.register("session1", affordance, 0, listener1) 71 | registry.register("session1", affordance, 0, listener2) 72 | registry.unregisterAll("session1") 73 | assertTrue(registry.registryMap["session1"]?.listeners == null) 74 | } 75 | 76 | @Test 77 | fun unregisterAllSessionsClearsAllListeners() { 78 | val registry = SessionAwareProtocolListenerRegistry() 79 | val affordance = StringProperty(forms = mutableListOf(Form(href = "http://example.com", contentType = "application/json"))) 80 | val listener = ContentListener { content -> println(content) } 81 | 82 | registry.register("session1", affordance, 0, listener) 83 | registry.register("session2", affordance, 0, listener) 84 | registry.unregisterAllSessions() 85 | assertTrue(registry.registryMap.isEmpty()) 86 | } 87 | 88 | @Test 89 | fun notifyHandlesFormIndexNullAcrossSessions() = runTest { 90 | val registry = SessionAwareProtocolListenerRegistry() 91 | val affordance = StringProperty(forms = mutableListOf(Form(href = "http://example.com", contentType = "application/json"))) 92 | val listener1 = mockk(relaxed = true) 93 | val listener2 = mockk(relaxed = true) 94 | 95 | registry.register("session1", affordance, 0, listener1) 96 | registry.register("session2", affordance, 0, listener2) 97 | registry.notify(affordance, "testContent".toInteractionInputValue(), affordance) 98 | 99 | coEvery { listener1.handle(any()) } 100 | coEvery { listener2.handle(any()) } 101 | } 102 | } -------------------------------------------------------------------------------- /kotlin-wot/src/test/kotlin/WotTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb 8 | 9 | import org.eclipse.thingweb.thing.ExposedThing 10 | import org.eclipse.thingweb.thing.ThingDescription 11 | import org.eclipse.thingweb.thing.filter.DiscoveryMethod 12 | import org.eclipse.thingweb.thing.filter.ThingFilter 13 | import io.mockk.* 14 | import kotlinx.coroutines.flow.flowOf 15 | import kotlinx.coroutines.flow.toList 16 | import kotlinx.coroutines.test.runTest 17 | import java.net.URI 18 | import kotlin.test.BeforeTest 19 | import kotlin.test.Test 20 | import kotlin.test.assertEquals 21 | import kotlin.test.assertFailsWith 22 | 23 | 24 | class WotTest { 25 | 26 | // Mocked dependency 27 | private lateinit var servient: Servient 28 | private lateinit var defaultWot: DefaultWot 29 | 30 | @BeforeTest 31 | fun setup() { 32 | servient = mockk() // Mock the Servient class 33 | defaultWot = DefaultWot(servient) 34 | } 35 | 36 | @Test 37 | fun `test discover with filter`() = runTest { 38 | // Given 39 | val filter = ThingFilter(method = DiscoveryMethod.ANY) 40 | val thing = mockk() 41 | coEvery { servient.discover(filter) } returns flowOf(thing) 42 | 43 | // When 44 | val result = defaultWot.discover(filter).toList() 45 | 46 | // Then 47 | assertEquals(1, result.size) 48 | assertEquals(thing, result.first()) 49 | coVerify { servient.discover(filter) } 50 | } 51 | 52 | @Test 53 | fun `test discover without filter`() = runTest { 54 | // Given 55 | val filter = ThingFilter(method = DiscoveryMethod.ANY) 56 | val thing = mockk() 57 | coEvery { servient.discover(filter) } returns flowOf(thing) 58 | 59 | // When 60 | val result = defaultWot.discover().toList() 61 | 62 | // Then 63 | assertEquals(1, result.size) 64 | assertEquals(thing, result.first()) 65 | coVerify { servient.discover(filter) } 66 | } 67 | 68 | @Test 69 | fun `test fetch with URI`() = runTest { 70 | // Given 71 | val url = URI("http://example.com") 72 | val thingDescription = mockk() 73 | coEvery { servient.fetch(url) } returns thingDescription 74 | 75 | // When 76 | val result = defaultWot.requestThingDescription(url) 77 | 78 | // Then 79 | assertEquals(thingDescription, result) 80 | coVerify { servient.fetch(url) } 81 | } 82 | 83 | @Test 84 | fun `test fetch with String URL`() = runTest { 85 | // Given 86 | val urlString = "http://example.com" 87 | val thingDescription = mockk() 88 | coEvery { servient.fetch(urlString) } returns thingDescription 89 | 90 | // When 91 | val result = defaultWot.requestThingDescription(urlString) 92 | 93 | // Then 94 | assertEquals(thingDescription, result) 95 | coVerify { servient.fetch(urlString) } 96 | } 97 | 98 | @Test 99 | fun `test destroy`() = runTest { 100 | // Given 101 | coEvery { servient.shutdown() } just runs 102 | 103 | // When 104 | defaultWot.destroy() 105 | 106 | // Then 107 | coVerify { servient.shutdown() } 108 | } 109 | 110 | 111 | @Test 112 | fun `produce should return ExposedThing when adding is successful`() = runTest { 113 | // Arrange 114 | 115 | // Mocking servient.addThing to return true, indicating the Thing is added successfully 116 | every { servient.addThing(ofType(ExposedThing::class)) } returns true 117 | 118 | // Act 119 | val exposedThing = defaultWot.produce{ 120 | title = "testTitle" 121 | stringProperty("propA") { 122 | title = "some title" 123 | } 124 | intProperty("propB") { 125 | title = "some title" 126 | } 127 | } 128 | 129 | // Assert 130 | assertEquals(exposedThing.getThingDescription().title, "testTitle") 131 | } 132 | 133 | @Test 134 | fun `produce should throw WotException when thing already exists`() = runTest { 135 | // Arrange 136 | val thingDescription = ThingDescription(id = "existingThing") 137 | 138 | // Mocking servient.addThing to return false, indicating the Thing already exists 139 | coEvery { servient.addThing(ofType(ExposedThing::class)) } returns false 140 | 141 | // Act and Assert 142 | val exception = assertFailsWith { 143 | defaultWot.produce(thingDescription) 144 | } 145 | assertEquals("Thing already exists: existingThing", exception.message) 146 | } 147 | } -------------------------------------------------------------------------------- /kotlin-wot/src/test/kotlin/thing/ProtocolListenerRegistryTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing 8 | 9 | import org.eclipse.thingweb.thing.form.Form 10 | import org.eclipse.thingweb.thing.schema.ContentListener 11 | import org.eclipse.thingweb.thing.schema.StringProperty 12 | import org.eclipse.thingweb.thing.schema.toInteractionInputValue 13 | import io.mockk.coEvery 14 | import io.mockk.mockk 15 | import kotlinx.coroutines.test.runTest 16 | import kotlin.test.* 17 | 18 | class ProtocolListenerRegistryTest { 19 | 20 | @Test 21 | fun registerAddsListenerSuccessfully() { 22 | val registry = ProtocolListenerRegistry() 23 | val affordance = StringProperty(forms = mutableListOf(Form(href = "http://example.com", contentType = "application/json"))) 24 | val listener = ContentListener { content -> println(content) } 25 | 26 | registry.register(affordance, 0, listener) 27 | assertNotNull(registry.listeners[affordance]?.get(0)) 28 | } 29 | 30 | @Test 31 | fun registerThrowsExceptionForInvalidFormIndex() { 32 | val registry = ProtocolListenerRegistry() 33 | val affordance = StringProperty(forms = mutableListOf(Form(href = "http://example.com", contentType = "application/json"))) 34 | val listener = ContentListener { content -> println(content) } 35 | 36 | assertFailsWith { 37 | registry.register(affordance, 1, listener) 38 | } 39 | } 40 | 41 | @Test 42 | fun unregisterRemovesListenerSuccessfully() { 43 | val registry = ProtocolListenerRegistry() 44 | val affordance = StringProperty(forms = mutableListOf(Form(href = "http://example.com", contentType = "application/json"))) 45 | val listener = ContentListener { content -> println(content) } 46 | 47 | registry.register(affordance, 0, listener) 48 | registry.unregister(affordance, 0,) 49 | assertNull(registry.listeners[affordance]?.get(0)) 50 | } 51 | 52 | @Test 53 | fun unregisterThrowsExceptionForNonExistentListener() { 54 | val registry = ProtocolListenerRegistry() 55 | val affordance = StringProperty(forms = mutableListOf(Form(href = "http://example.com", contentType = "application/json"))) 56 | val listener = ContentListener { content -> println(content) } 57 | 58 | assertFailsWith { 59 | registry.unregister(affordance, 0) 60 | } 61 | } 62 | 63 | @Test 64 | fun unregisterAllClearsAllListeners() { 65 | val registry = ProtocolListenerRegistry() 66 | val affordance = StringProperty(forms = mutableListOf(Form(href = "http://example.com", contentType = "application/json"))) 67 | val listener = ContentListener { content -> println(content) } 68 | 69 | registry.register(affordance, 0, listener) 70 | registry.unregisterAll() 71 | assertTrue(registry.listeners.isEmpty()) 72 | } 73 | 74 | @Test 75 | fun notifyCallsListenersWithCorrectContent() = runTest { 76 | val registry = ProtocolListenerRegistry() 77 | val affordance = StringProperty(forms = mutableListOf(Form(href = "http://example.com", contentType = "application/json"))) 78 | val listener = mockk(relaxed = true) 79 | 80 | registry.register(affordance, 0, listener) 81 | registry.notify(affordance, "testContent".toInteractionInputValue(), affordance, 0) 82 | 83 | coEvery { listener.handle(any()) } 84 | } 85 | 86 | @Test 87 | fun notifyCallsAllListenersForAffordance() = runTest { 88 | val registry = ProtocolListenerRegistry() 89 | val affordance = StringProperty(forms = mutableListOf(Form(href = "http://example.com", contentType = "application/json"))) 90 | val listener1 = mockk(relaxed = true) 91 | val listener2 = mockk(relaxed = true) 92 | 93 | registry.register(affordance, 0, listener1) 94 | registry.register(affordance, 0, listener2) 95 | registry.notify(affordance, "testContent".toInteractionInputValue(), affordance, 0) 96 | 97 | coEvery { listener1.handle(any()) } 98 | coEvery { listener2.handle(any()) } 99 | } 100 | 101 | @Test 102 | fun notifyHandlesNullFormIndex() = runTest { 103 | val registry = ProtocolListenerRegistry() 104 | val affordance = StringProperty(forms = mutableListOf(Form(href = "http://example.com", contentType = "application/json"))) 105 | val listener = mockk(relaxed = true) 106 | 107 | registry.register(affordance, 0, listener) 108 | registry.notify(affordance, "testContent".toInteractionInputValue(), affordance) 109 | 110 | coEvery { listener.handle(any()) } 111 | } 112 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/ProtocolListenerRegistry.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.thing 8 | 9 | import org.eclipse.thingweb.content.ContentManager 10 | import org.eclipse.thingweb.thing.schema.ContentListener 11 | import org.eclipse.thingweb.thing.schema.DataSchema 12 | import org.eclipse.thingweb.thing.schema.InteractionAffordance 13 | import org.eclipse.thingweb.thing.schema.InteractionInput 14 | import org.slf4j.Logger 15 | import org.slf4j.LoggerFactory 16 | import java.util.concurrent.ConcurrentHashMap 17 | import java.util.concurrent.ConcurrentMap 18 | import kotlin.coroutines.cancellation.CancellationException 19 | 20 | class ProtocolListenerRegistry { 21 | 22 | private val log: Logger = LoggerFactory.getLogger(ProtocolListenerRegistry::class.java) 23 | 24 | // Map affordances to form indices and their associated listener 25 | internal val listeners: ConcurrentMap> = ConcurrentHashMap() 26 | 27 | /** 28 | * Registers a listener for a specific affordance and form index. 29 | * If a listener already exists for the specified form index, it will be replaced. 30 | * 31 | * @param affordance The interaction affordance. 32 | * @param formIndex The index of the form. 33 | * @param listener The listener to register. 34 | */ 35 | fun register(affordance: InteractionAffordance, formIndex: Int, listener: ContentListener) { 36 | val form = affordance.forms.getOrNull(formIndex) 37 | ?: throw IllegalArgumentException("Can't register listener; no form at index $formIndex for the affordance") 38 | 39 | val formMap = listeners.getOrPut(affordance) { mutableMapOf() } 40 | formMap[formIndex] = listener // Replaces any existing listener for the form index 41 | } 42 | 43 | /** 44 | * Unregisters the listener for a specific affordance and form index. 45 | * 46 | * @param affordance The interaction affordance. 47 | * @param formIndex The index of the form. 48 | * @throws IllegalStateException if no listener is found for the affordance or form index. 49 | */ 50 | fun unregister(affordance: InteractionAffordance, formIndex: Int) { 51 | val formMap = listeners[affordance] ?: throw IllegalStateException("Listener not found for affordance") 52 | val wasRemoved = formMap.remove(formIndex) != null 53 | if (!wasRemoved) throw IllegalStateException("Listener not found at form index $formIndex") 54 | } 55 | 56 | /** 57 | * Unregisters all listeners for all affordances. 58 | */ 59 | fun unregisterAll() { 60 | listeners.clear() 61 | } 62 | 63 | /** 64 | * Notifies the listener for a given affordance and optional form index. 65 | * 66 | * @param affordance The interaction affordance. 67 | * @param data The input data. 68 | * @param schema The schema for validation (optional). 69 | * @param formIndex The index of the form to notify the listener for (optional). 70 | */ 71 | suspend fun notify( 72 | affordance: InteractionAffordance, 73 | data: InteractionInput, 74 | schema: DataSchema? = null, 75 | formIndex: Int? = null 76 | ) { 77 | log.debug("Notify listeners for affordance with title '${affordance.title}'") 78 | val formMap = listeners[affordance] ?: emptyMap() 79 | 80 | val interactionInputValue = data as InteractionInput.Value 81 | 82 | if (formIndex != null) { 83 | formMap[formIndex]?.let { listener -> 84 | val contentType = affordance.forms[formIndex].contentType 85 | try { 86 | val content = ContentManager.valueToContent(interactionInputValue.value, contentType) 87 | listener.handle(content) 88 | } catch (e: CancellationException) { 89 | log.info("Cancellation exception while notifying listener", e) 90 | throw e // Rethrow cancellation exception 91 | } catch (e: Exception) { 92 | log.error("Error while notifying listener", e) 93 | } 94 | } 95 | return 96 | } 97 | 98 | formMap.forEach { (index, listener) -> 99 | log.debug("Notify listener for form {}", affordance.forms[index]) 100 | val contentType = affordance.forms[index].contentType 101 | try { 102 | val content = ContentManager.valueToContent(interactionInputValue.value, contentType) 103 | listener.handle(content) 104 | } catch (e: CancellationException) { 105 | log.info("Cancellation exception while notifying listener", e) 106 | throw e // Rethrow cancellation exception 107 | } catch (e: Exception) { 108 | log.error("Error while notifying listener", e) 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /kotlin-wot/src/main/kotlin/thing/form/AugmentedForm.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import com.fasterxml.jackson.annotation.JsonIgnore 8 | import com.fasterxml.jackson.databind.JsonNode 9 | import org.eclipse.thingweb.security.SecurityScheme 10 | import org.eclipse.thingweb.thing.form.Form 11 | import org.eclipse.thingweb.thing.schema.WoTForm 12 | import org.eclipse.thingweb.thing.schema.WoTThingDescription 13 | import org.slf4j.LoggerFactory 14 | import java.net.URI 15 | import java.net.URISyntaxException 16 | 17 | /** 18 | * A [Form] augmented with information from its associated [thingDescription] 19 | * and [interactionAffordance]. 20 | */ 21 | data class AugmentedForm( 22 | private val form: WoTForm, 23 | private val thingDescription: WoTThingDescription 24 | ) : WoTForm by form { 25 | 26 | companion object { 27 | private val log = LoggerFactory.getLogger(AugmentedForm::class.java) 28 | } 29 | 30 | @get:JsonIgnore 31 | val hrefScheme: String? 32 | get() = try { 33 | // remove uri variables first 34 | val sanitizedHref = href.substringBefore("{") 35 | URI(sanitizedHref).scheme 36 | } catch (e: URISyntaxException) { 37 | log.warn("Form href is invalid: ", e) 38 | null 39 | } 40 | 41 | override val href: String 42 | get() { 43 | val href = thingDescription.base?.let { 44 | // Remove trailing slash from base if it exists and leading slash from href if it exists 45 | val baseUrl = it.trimEnd('/') // Remove trailing slash from base 46 | val formHref = form.href.trimStart('/') // Remove leading slash from href 47 | "$baseUrl/$formHref" // Concatenate with a single slash in between 48 | } ?: form.href 49 | val hrefUriVariables = filterUriVariables(href) 50 | 51 | if (hrefUriVariables.isEmpty()) return href 52 | 53 | return href 54 | /* 55 | 56 | val affordanceUriVariables = mutableMapOf().apply { 57 | putAll(thingDescription.uriVariables ?: emptyMap()) 58 | putAll(interactionAffordance.uriVariables ?: emptyMap()) 59 | } 60 | 61 | userProvidedUriVariables?.let { 62 | validateUriVariables(hrefUriVariables, affordanceUriVariables, it) 63 | } 64 | 65 | val decodedHref = href.toString().decode() 66 | val expandedHref = UriTemplate(decodedHref).expand(userProvidedUriVariables ?: emptyMap()) 67 | return URI(expandedHref) 68 | 69 | */ 70 | } 71 | 72 | /** 73 | * The computed list of [SecurityScheme]s associated with this form. 74 | */ 75 | val securityDefinitions: List 76 | get() = thingDescription.securityDefinitions.entries 77 | .filter { form.security?.contains(it.key) ?: false } 78 | .map { it.value } 79 | 80 | private fun filterUriVariables(href: String): List { 81 | val regex = Regex("\\{[?+#./;&]?([^}]+)\\}") // Extracts text inside `{}` while ignoring optional prefix 82 | return regex.findAll(href) 83 | .map { it.groupValues[1] } // Directly access group value (instead of using `?.value`) 84 | .flatMap { it.split(",") } // Handle multiple variables inside `{}` (comma-separated) 85 | .map { it.trim() } // Ensure clean variable names 86 | .toList() 87 | } 88 | 89 | private fun validateUriVariables( 90 | uriVariablesInHref: List, 91 | affordanceUriVariables: Map, 92 | userProvidedUriVariables: Map 93 | ) { 94 | val uncoveredHrefUriVariables = uriVariablesInHref.filterNot { affordanceUriVariables.containsKey(it) } 95 | 96 | if (uncoveredHrefUriVariables.isNotEmpty()) { 97 | throw IllegalArgumentException( 98 | "The following URI template variables defined in the form's href " + 99 | "but are not covered by a uriVariable entry at the TD or affordance " + 100 | "level: ${uncoveredHrefUriVariables.joinToString(", ")}.") 101 | } 102 | 103 | /* 104 | affordanceUriVariables.forEach { (key, schemaValue) -> 105 | userProvidedUriVariables[key]?.let { userProvidedValue -> 106 | val schema = JsonSchema.create(schemaValue.toJson()) 107 | val result = schema.validate(userProvidedValue) 108 | if (!result.isValid) { 109 | throw IllegalArgumentException("Invalid type for URI variable $key") 110 | } 111 | } 112 | } 113 | */ 114 | } 115 | 116 | override fun equals(other: Any?): Boolean { 117 | if (this === other) return true 118 | if (javaClass != other?.javaClass) return false 119 | 120 | other as AugmentedForm 121 | 122 | return form == other.form 123 | } 124 | 125 | override fun hashCode(): Int { 126 | return form.hashCode() 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /kotlin-wot/src/test/kotlin/content/ContentManagerTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Robert Winkler 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package org.eclipse.thingweb.content 8 | 9 | import org.eclipse.thingweb.thing.schema.ObjectSchema 10 | import org.eclipse.thingweb.thing.schema.StringSchema 11 | import io.mockk.every 12 | import io.mockk.mockk 13 | import org.junit.jupiter.api.Assertions.* 14 | import org.junit.jupiter.api.BeforeEach 15 | import org.junit.jupiter.api.Test 16 | import org.junit.jupiter.api.assertThrows 17 | import kotlin.test.assertFailsWith 18 | 19 | class ContentManagerTest { 20 | 21 | @BeforeEach 22 | fun setup() { 23 | // Reset CODECS and OFFERED maps 24 | ContentManager.removeCodec("application/json") 25 | } 26 | 27 | @Test 28 | fun `addCodec should register a codec and add to OFFERED if offered is true`() { 29 | // Act 30 | ContentManager.addCodec(JsonCodec(), true) 31 | 32 | // Assert 33 | assertTrue(ContentManager.offeredMediaTypes.contains("application/json")) 34 | assertTrue(ContentManager.isSupportedMediaType("application/json")) 35 | } 36 | 37 | @Test 38 | fun `removeCodec should unregister codec and remove from OFFERED set`() { 39 | // Arrange 40 | ContentManager.addCodec(JsonCodec(), true) 41 | 42 | // Act 43 | ContentManager.removeCodec("application/json") 44 | 45 | // Assert 46 | assertFalse(ContentManager.offeredMediaTypes.contains("application/json")) 47 | assertFalse(ContentManager.isSupportedMediaType("application/json")) 48 | } 49 | 50 | @Test 51 | fun `contentToValue should use codec to convert content to value`() { 52 | ContentManager.addCodec(JsonCodec(), true) 53 | 54 | // Arrange 55 | val testContent = Content(type = "application/json", body = """{"key": "value"}""".toByteArray()) 56 | 57 | // Act 58 | val result = ContentManager.contentToValue(testContent, ObjectSchema()) 59 | 60 | // Assert 61 | //assertEquals("value", result.value["key"]) 62 | } 63 | 64 | @Test 65 | fun `contentToValue should throw exception when codec fails to decode`() { 66 | // Arrange 67 | val jsonCodec : ContentCodec = mockk() 68 | every { jsonCodec.mediaType } returns "application/json" 69 | ContentManager.addCodec(jsonCodec, true) 70 | val testContent = Content(type = "application/json", body = """{"key": "value"}""".toByteArray()) 71 | 72 | every { jsonCodec.bytesToValue(testContent.body, StringSchema(), any()) } throws ContentCodecException("Decode error") 73 | 74 | // Act & Assert 75 | val exception = assertThrows { 76 | ContentManager.contentToValue(testContent, StringSchema()) 77 | } 78 | assertEquals("Decode error", exception.message) 79 | } 80 | 81 | @Test 82 | fun `valueToContent should use codec to serialize value to Content`() { 83 | // Arrange 84 | ContentManager.addCodec(JsonCodec(), true) 85 | 86 | val testValue = "value" 87 | 88 | // Act 89 | val result = ContentManager.valueToContent(testValue, "application/json") 90 | val stringValue = ContentManager.contentToValue(result, StringSchema()) 91 | 92 | // Assert 93 | assertEquals(testValue, stringValue.textValue()) 94 | } 95 | 96 | @Test 97 | fun `valueToContent should use codec to serialize data scheme value to Content`() { 98 | // Arrange 99 | ContentManager.addCodec(JsonCodec(), true) 100 | 101 | val testValue = "value" 102 | 103 | // Act 104 | val result = ContentManager.valueToContent(testValue, "application/json") 105 | val stringValue = ContentManager.contentToValue(result, StringSchema()) 106 | 107 | // Assert 108 | assertEquals(testValue, stringValue.textValue()) 109 | } 110 | 111 | @Test 112 | fun `valueToContent should throw exception when codec fails to encode`() { 113 | val jsonCodec : ContentCodec = mockk() 114 | // Arrange 115 | val testValue = "value to encode" 116 | every { jsonCodec.mediaType } returns "application/json" 117 | ContentManager.addCodec(jsonCodec, true) 118 | every { jsonCodec.valueToBytes(testValue, any()) } throws ContentCodecException("Encode error") 119 | 120 | // Act & Assert 121 | val exception = assertFailsWith { 122 | ContentManager.valueToContent(testValue, "application/json") 123 | } 124 | assertEquals("Encode error", exception.message) 125 | } 126 | 127 | @Test 128 | fun `getMediaType should extract correct media type`() { 129 | // Act 130 | val result = ContentManager.getMediaType("text/plain; charset=utf-8") 131 | 132 | // Assert 133 | assertEquals("text/plain", result) 134 | } 135 | 136 | @Test 137 | fun `getMediaTypeParameters should extract parameters correctly`() { 138 | // Act 139 | val result = ContentManager.getMediaTypeParameters("text/plain; charset=utf-8; boundary=something") 140 | 141 | // Assert 142 | assertEquals(2, result.size) 143 | assertEquals("utf-8", result["charset"]) 144 | assertEquals("something", result["boundary"]) 145 | } 146 | } --------------------------------------------------------------------------------