From 9e243a469ae6544e8cf523ad09b959f541c3f565 Mon Sep 17 00:00:00 2001 From: Robert Jaros Date: Wed, 11 Mar 2020 16:18:17 +0100 Subject: Upgrade to Kotlin 1.3.70 + other dependencies (Coroutinse, Serialization, Spring Boot) Major refactoring of build architecture. --- .../kvision-server-spring-boot/build.gradle | 40 -- .../kvision-server-spring-boot/build.gradle.kts | 72 +++ .../pl/treksoft/kvision/remote/Annotations.kt | 30 ++ .../pl/treksoft/kvision/remote/KVServiceManager.kt | 122 +++++ .../pl/treksoft/kvision/remote/KVServiceManager.kt | 160 ++++++ .../pl/treksoft/kvision/remote/Annotations.kt | 29 + .../kvision/remote/KVRouterConfiguration.kt | 81 +++ .../pl/treksoft/kvision/remote/KVServiceManager.kt | 586 +++++++++++++++++++++ .../treksoft/kvision/remote/KVWebSocketConfig.kt | 110 ++++ .../kotlin/pl/treksoft/kvision/remote/Security.kt | 67 +++ .../treksoft/kvision/remote/SessionInterfaces.kt | 43 ++ .../jvmMain/resources/META-INF/spring.factories | 1 + .../pl/treksoft/kvision/remote/Annotations.kt | 29 - .../kvision/remote/KVRouterConfiguration.kt | 81 --- .../pl/treksoft/kvision/remote/KVServiceManager.kt | 586 --------------------- .../treksoft/kvision/remote/KVWebSocketConfig.kt | 108 ---- .../kotlin/pl/treksoft/kvision/remote/Security.kt | 67 --- .../treksoft/kvision/remote/SessionInterfaces.kt | 43 -- .../main/kotlin/pl/treksoft/kvision/types/Date.kt | 200 ------- .../kotlin/pl/treksoft/kvision/types/Decimal.kt | 47 -- .../src/main/resources/META-INF/spring.factories | 1 - 21 files changed, 1301 insertions(+), 1202 deletions(-) delete mode 100644 kvision-modules/kvision-server-spring-boot/build.gradle create mode 100644 kvision-modules/kvision-server-spring-boot/build.gradle.kts create mode 100644 kvision-modules/kvision-server-spring-boot/src/commonMain/kotlin/pl/treksoft/kvision/remote/Annotations.kt create mode 100644 kvision-modules/kvision-server-spring-boot/src/commonMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt create mode 100644 kvision-modules/kvision-server-spring-boot/src/jsMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt create mode 100644 kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/Annotations.kt create mode 100644 kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVRouterConfiguration.kt create mode 100644 kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt create mode 100644 kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVWebSocketConfig.kt create mode 100644 kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/Security.kt create mode 100644 kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/SessionInterfaces.kt create mode 100644 kvision-modules/kvision-server-spring-boot/src/jvmMain/resources/META-INF/spring.factories delete mode 100644 kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/Annotations.kt delete mode 100644 kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVRouterConfiguration.kt delete mode 100644 kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt delete mode 100644 kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVWebSocketConfig.kt delete mode 100644 kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/Security.kt delete mode 100644 kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/SessionInterfaces.kt delete mode 100644 kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/types/Date.kt delete mode 100644 kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/types/Decimal.kt delete mode 100644 kvision-modules/kvision-server-spring-boot/src/main/resources/META-INF/spring.factories (limited to 'kvision-modules/kvision-server-spring-boot') diff --git a/kvision-modules/kvision-server-spring-boot/build.gradle b/kvision-modules/kvision-server-spring-boot/build.gradle deleted file mode 100644 index c46a38b1..00000000 --- a/kvision-modules/kvision-server-spring-boot/build.gradle +++ /dev/null @@ -1,40 +0,0 @@ -apply plugin: 'kotlin-platform-jvm' -apply plugin: 'kotlinx-serialization' - -repositories { - mavenCentral() - jcenter() - maven { url = "https://dl.bintray.com/kotlin/kotlin-eap" } - maven { url = 'https://kotlin.bintray.com/kotlinx' } - maven { url = 'https://dl.bintray.com/rjaros/kotlin' } - maven { url = "https://repo.spring.io/milestone" } -} - -dependencies { - expectedBy project(":kvision-modules:kvision-common-types") - expectedBy project(":kvision-modules:kvision-common-remote") - expectedBy project(":kvision-modules:kvision-common-annotations") - compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - compile "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serializationVersion" - compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" - compile "org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$coroutinesVersion" - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" - compile "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" - compile "org.springframework.boot:spring-boot-starter:$springBootVersion" - compile "org.springframework.boot:spring-boot-starter-webflux:$springBootVersion" - compile "org.springframework.boot:spring-boot-starter-security:$springBootVersion" - compile "org.springframework.data:spring-data-relational:$springDataRelationalVersion" - compile "com.fasterxml.jackson.module:jackson-module-kotlin:${jacksonModuleKotlinVersion}" - testCompile "org.jetbrains.kotlin:kotlin-test:$kotlinVersion" - testCompile project(":kvision-modules:kvision-common-types") - testCompile project(":kvision-modules:kvision-common-remote") - testCompile project(":kvision-modules:kvision-common-annotations") -} - -compileKotlin { - kotlinOptions { - freeCompilerArgs = ["-Xjsr305=strict"] - jvmTarget = "1.8" - } -} diff --git a/kvision-modules/kvision-server-spring-boot/build.gradle.kts b/kvision-modules/kvision-server-spring-boot/build.gradle.kts new file mode 100644 index 00000000..d24073d4 --- /dev/null +++ b/kvision-modules/kvision-server-spring-boot/build.gradle.kts @@ -0,0 +1,72 @@ +buildscript { + extra.set("production", (findProperty("prod") ?: findProperty("production") ?: "false") == "true") +} + +plugins { + kotlin("multiplatform") + id("kotlinx-serialization") + id("maven-publish") +} + +repositories() + +// Versions +val kotlinVersion: String by System.getProperties() +val serializationVersion: String by project +val coroutinesVersion: String by project +val springBootVersion: String by project +val springDataRelationalVersion: String by project +val jacksonModuleKotlinVersion: String by project + +kotlin { + kotlinJsTargets() + kotlinJvmTargets() + sourceSets { + val commonMain by getting { + dependencies { + implementation(kotlin("stdlib-common")) + api(project(":kvision-modules:kvision-common-annotations")) + api(project(":kvision-modules:kvision-common-types")) + api(project(":kvision-modules:kvision-common-remote")) + api(project(":kvision-modules:kvision-common-remote")) + api("org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serializationVersion") + api("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutinesVersion") + } + } + val jsMain by getting { + dependencies { + implementation(kotlin("stdlib-js")) + api("org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serializationVersion") + api("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:$coroutinesVersion") + } + } + val jvmMain by getting { + dependsOn(commonMain) + dependencies { + implementation(kotlin("stdlib")) + implementation(kotlin("stdlib-jdk7")) + implementation(kotlin("stdlib-jdk8")) + implementation(kotlin("reflect")) + api("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serializationVersion") + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + api("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$coroutinesVersion") + api("org.springframework.boot:spring-boot-starter:$springBootVersion") + api("org.springframework.boot:spring-boot-starter-webflux:$springBootVersion") + api("org.springframework.boot:spring-boot-starter-security:$springBootVersion") + api("org.springframework.data:spring-data-relational:$springDataRelationalVersion") + api("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonModuleKotlinVersion") + } + } + } +} + +publishing { + publications.withType { + if (name == "kotlinMultiplatform") artifactId = "kvision-server-spring-boot" + pom { + defaultPom() + } + } +} + +setupPublication() diff --git a/kvision-modules/kvision-server-spring-boot/src/commonMain/kotlin/pl/treksoft/kvision/remote/Annotations.kt b/kvision-modules/kvision-server-spring-boot/src/commonMain/kotlin/pl/treksoft/kvision/remote/Annotations.kt new file mode 100644 index 00000000..67fb1de1 --- /dev/null +++ b/kvision-modules/kvision-server-spring-boot/src/commonMain/kotlin/pl/treksoft/kvision/remote/Annotations.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package pl.treksoft.kvision.remote + +@OptIn(ExperimentalMultiplatform::class) +@OptionalExpectation +expect annotation class Id() + +@OptIn(ExperimentalMultiplatform::class) +@OptionalExpectation +expect annotation class Transient() diff --git a/kvision-modules/kvision-server-spring-boot/src/commonMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt b/kvision-modules/kvision-server-spring-boot/src/commonMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt new file mode 100644 index 00000000..19da13c0 --- /dev/null +++ b/kvision-modules/kvision-server-spring-boot/src/commonMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package pl.treksoft.kvision.remote + +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import kotlin.reflect.KClass + +/** + * Multiplatform service manager. + */ +expect open class KVServiceManager(serviceClass: KClass) { + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + protected inline fun bind( + noinline function: suspend T.() -> RET, + method: HttpMethod = HttpMethod.POST, + route: String? = null + ) + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + protected inline fun bind( + noinline function: suspend T.(PAR) -> RET, + method: HttpMethod = HttpMethod.POST, + route: String? = null + ) + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + protected inline fun bind( + noinline function: suspend T.(PAR1, PAR2) -> RET, + method: HttpMethod = HttpMethod.POST, + route: String? = null + ) + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + protected inline fun bind( + noinline function: suspend T.(PAR1, PAR2, PAR3) -> RET, + method: HttpMethod = HttpMethod.POST, + route: String? = null + ) + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + protected inline fun bind( + noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4) -> RET, + method: HttpMethod = HttpMethod.POST, + route: String? = null + ) + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + protected inline fun bind( + noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4, PAR5) -> RET, + method: HttpMethod = HttpMethod.POST, + route: String? = null + ) + + /** + * Binds a given function of the receiver as a tabulator component source + * @param function a function of the receiver + */ + protected inline fun bindTabulatorRemote( + noinline function: suspend T.(Int?, Int?, List?, List?, String?) -> RemoteData + ) + + /** + * Binds a given function of the receiver as a web socket connection + * @param function a function of the receiver + */ + protected inline fun bind( + noinline function: suspend T.(ReceiveChannel, SendChannel) -> Unit, + route: String? = null + ) +} diff --git a/kvision-modules/kvision-server-spring-boot/src/jsMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt b/kvision-modules/kvision-server-spring-boot/src/jsMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt new file mode 100644 index 00000000..23da6ac5 --- /dev/null +++ b/kvision-modules/kvision-server-spring-boot/src/jsMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package pl.treksoft.kvision.remote + +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import kotlin.reflect.KClass + +/** + * Multiplatform service manager. + */ +actual open class KVServiceManager actual constructor(serviceClass: KClass): KVServiceMgr { + + protected val calls: MutableMap> = mutableMapOf() + var counter: Int = 0 + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + protected actual inline fun bind( + noinline function: suspend T.() -> RET, + method: HttpMethod, route: String? + ) { + val routeDef = route ?: "route${this::class.simpleName}${counter++}" + calls[function.toString().replace("\\s".toRegex(), "")] = Pair("/kv/$routeDef", method) + } + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + protected actual inline fun bind( + noinline function: suspend T.(PAR) -> RET, + method: HttpMethod, route: String? + ) { + if (method == HttpMethod.GET) + throw UnsupportedOperationException("GET method is only supported for methods without parameters") + val routeDef = route ?: "route${this::class.simpleName}${counter++}" + calls[function.toString().replace("\\s".toRegex(), "")] = Pair("/kv/$routeDef", method) + } + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + protected actual inline fun bind( + noinline function: suspend T.(PAR1, PAR2) -> RET, + method: HttpMethod, route: String? + ) { + if (method == HttpMethod.GET) + throw UnsupportedOperationException("GET method is only supported for methods without parameters") + val routeDef = route ?: "route${this::class.simpleName}${counter++}" + calls[function.toString().replace("\\s".toRegex(), "")] = Pair("/kv/$routeDef", method) + } + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + protected actual inline fun bind( + noinline function: suspend T.(PAR1, PAR2, PAR3) -> RET, + method: HttpMethod, route: String? + ) { + if (method == HttpMethod.GET) + throw UnsupportedOperationException("GET method is only supported for methods without parameters") + val routeDef = route ?: "route${this::class.simpleName}${counter++}" + calls[function.toString().replace("\\s".toRegex(), "")] = Pair("/kv/$routeDef", method) + } + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + protected actual inline fun bind( + noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4) -> RET, + method: HttpMethod, route: String? + ) { + if (method == HttpMethod.GET) + throw UnsupportedOperationException("GET method is only supported for methods without parameters") + val routeDef = route ?: "route${this::class.simpleName}${counter++}" + calls[function.toString().replace("\\s".toRegex(), "")] = Pair("/kv/$routeDef", method) + } + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + protected actual inline fun bind( + noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4, PAR5) -> RET, + method: HttpMethod, route: String? + ) { + if (method == HttpMethod.GET) + throw UnsupportedOperationException("GET method is only supported for methods without parameters") + val routeDef = route ?: "route${this::class.simpleName}${counter++}" + calls[function.toString().replace("\\s".toRegex(), "")] = Pair("/kv/$routeDef", method) + } + + /** + * Binds a given function of the receiver as a tabulator component source + * @param function a function of the receiver + */ + protected actual inline fun bindTabulatorRemote( + noinline function: suspend T.(Int?, Int?, List?, List?, String?) -> RemoteData + ) { + val routeDef = "route${this::class.simpleName}${counter++}" + calls[function.toString().replace("\\s".toRegex(), "")] = Pair("/kv/$routeDef", HttpMethod.POST) + } + + /** + * Binds a given web socket connetion with a function of the receiver. + * @param function a function of the receiver + * @param route a web socket route + */ + protected actual inline fun bind( + noinline function: suspend T.(ReceiveChannel, SendChannel) -> Unit, + route: String? + ) { + val routeDef = "route${this::class.simpleName}${counter++}" + calls[function.toString().replace("\\s".toRegex(), "")] = Pair("/kvws/$routeDef", HttpMethod.POST) + } + + /** + * Returns the map of defined paths. + */ + override fun getCalls(): Map> = calls + +} diff --git a/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/Annotations.kt b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/Annotations.kt new file mode 100644 index 00000000..7de82631 --- /dev/null +++ b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/Annotations.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package pl.treksoft.kvision.remote + +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Transient + +actual typealias Id = Id + +actual typealias Transient = Transient diff --git a/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVRouterConfiguration.kt b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVRouterConfiguration.kt new file mode 100644 index 00000000..42c4107d --- /dev/null +++ b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVRouterConfiguration.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package pl.treksoft.kvision.remote + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.support.GenericApplicationContext +import org.springframework.core.io.Resource +import org.springframework.http.MediaType.TEXT_HTML +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.server.ServerRequest +import org.springframework.web.reactive.function.server.ServerResponse +import org.springframework.web.reactive.function.server.buildAndAwait +import org.springframework.web.reactive.function.server.coRouter +import org.springframework.web.reactive.function.server.router + +@Configuration +open class KVRouterConfiguration { + @Value("classpath:/public/index.html") + private lateinit var indexHtml: Resource + + @Bean + open fun kvRoutes(kvHandler: KVHandler) = coRouter { + GET("/kv/**", kvHandler::handle) + POST("/kv/**", kvHandler::handle) + PUT("/kv/**", kvHandler::handle) + DELETE("/kv/**", kvHandler::handle) + OPTIONS("/kv/**", kvHandler::handle) + } + + @Bean + open fun indexRouter() = router { + GET("/") { + ok().contentType(TEXT_HTML).bodyValue(indexHtml) + } + } +} + +@Component +open class KVHandler(var services: List>, var applicationContext: ApplicationContext) { + + open suspend fun handle(request: ServerRequest): ServerResponse { + val routeUrl = request.path() + val handler = services.mapNotNull { + when (request.method()?.name) { + "GET" -> it.getRequests[routeUrl] + "POST" -> it.postRequests[routeUrl] + "PUT" -> it.putRequests[routeUrl] + "DELETE" -> it.deleteRequests[routeUrl] + "OPTIONS" -> it.optionsRequests[routeUrl] + else -> null + } + }.firstOrNull() + return if (handler != null) { + handler(request, applicationContext as GenericApplicationContext) + } else { + ServerResponse.notFound().buildAndAwait() + } + } +} diff --git a/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt new file mode 100644 index 00000000..4c1d8e6a --- /dev/null +++ b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt @@ -0,0 +1,586 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package pl.treksoft.kvision.remote + +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.reactive.awaitSingle +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.context.ApplicationContext +import org.springframework.web.reactive.function.server.ServerRequest +import org.springframework.web.reactive.function.server.ServerResponse +import org.springframework.web.reactive.function.server.awaitBody +import org.springframework.web.reactive.function.server.bodyValueAndAwait +import org.springframework.web.reactive.function.server.json +import org.springframework.web.reactive.socket.WebSocketSession +import pl.treksoft.kvision.types.* +import java.math.BigDecimal +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.OffsetDateTime +import java.time.OffsetTime +import kotlin.reflect.KClass + +/** + * Multiplatform service manager for Spring Boot. + */ +@OptIn(ExperimentalCoroutinesApi::class) +@Suppress("LargeClass", "TooManyFunctions") +actual open class KVServiceManager actual constructor(val serviceClass: KClass) { + + companion object { + val LOG: Logger = LoggerFactory.getLogger(KVServiceManager::class.java.name) + } + + val getRequests: MutableMap ServerResponse> = + mutableMapOf() + val postRequests: MutableMap ServerResponse> = + mutableMapOf() + val putRequests: MutableMap ServerResponse> = + mutableMapOf() + val deleteRequests: MutableMap ServerResponse> = + mutableMapOf() + val optionsRequests: MutableMap ServerResponse> = + mutableMapOf() + val webSocketsRequests: MutableMap, SendChannel + ) -> Unit> = + mutableMapOf() + + val mapper = jacksonObjectMapper().apply { + val module = SimpleModule() + module.addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer()) + module.addSerializer(LocalDate::class.java, LocalDateSerializer()) + module.addSerializer(LocalTime::class.java, LocalTimeSerializer()) + module.addSerializer(OffsetDateTime::class.java, OffsetDateTimeSerializer()) + module.addSerializer(OffsetTime::class.java, OffsetTimeSerializer()) + module.addSerializer(BigDecimal::class.java, BigDecimalSerializer()) + module.addDeserializer(LocalDateTime::class.java, LocalDateTimeDeserializer()) + module.addDeserializer(LocalDate::class.java, LocalDateDeserializer()) + module.addDeserializer(LocalTime::class.java, LocalTimeDeserializer()) + module.addDeserializer(OffsetDateTime::class.java, OffsetDateTimeDeserializer()) + module.addDeserializer(OffsetTime::class.java, OffsetTimeDeserializer()) + module.addDeserializer(BigDecimal::class.java, BigDecimalDeserializer()) + this.registerModule(module) + } + var counter: Int = 0 + + /** + * @suppress internal function + */ + suspend fun initializeService(service: T, req: ServerRequest) { + if (service is WithRequest) { + service.serverRequest = req + } + if (service is WithWebSession) { + val session = req.session().awaitSingle() + service.webSession = session + } + if (service is WithPrincipal) { + val principal = req.principal().awaitSingle() + service.principal = principal + } + } + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + @Suppress("TooGenericExceptionCaught") + protected actual inline fun bind( + noinline function: suspend T.() -> RET, + method: HttpMethod, route: String? + ) { + val routeDef = route ?: "route${this::class.simpleName}${counter++}" + addRoute(method, "/kv/$routeDef") { req, ctx -> + val service = ctx.getBean(serviceClass.java) + initializeService(service, req) + val jsonRpcRequest = if (method == HttpMethod.GET) { + JsonRpcRequest(req.queryParam("id").orElse(null)?.toInt() ?: 0, "", listOf()) + } else { + req.awaitBody() + } + try { + val result = function.invoke(service) + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + result = mapper.writeValueAsString(result) + ) + ) + ) + } catch (e: Exception) { + if (e !is ServiceException) LOG.error(e.message, e) + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + error = e.message ?: "Error", + exceptionType = e.javaClass.canonicalName + ) + ) + ) + } + } + } + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + @Suppress("TooGenericExceptionCaught") + protected actual inline fun bind( + noinline function: suspend T.(PAR) -> RET, + method: HttpMethod, route: String? + ) { + if (method == HttpMethod.GET) + throw UnsupportedOperationException("GET method is only supported for methods without parameters") + val routeDef = route ?: "route${this::class.simpleName}${counter++}" + addRoute(method, "/kv/$routeDef") { req, ctx -> + val service = ctx.getBean(serviceClass.java) + initializeService(service, req) + val jsonRpcRequest = req.awaitBody() + if (jsonRpcRequest.params.size == 1) { + val param = getParameter(jsonRpcRequest.params[0]) + try { + val result = function.invoke(service, param) + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + result = mapper.writeValueAsString(result) + ) + ) + ) + } catch (e: Exception) { + if (e !is ServiceException) LOG.error(e.message, e) + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + error = e.message ?: "Error", + exceptionType = e.javaClass.canonicalName + ) + ) + ) + } + } else { + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + error = "Invalid parameters" + ) + ) + ) + } + } + } + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + @Suppress("TooGenericExceptionCaught") + protected actual inline fun bind( + noinline function: suspend T.(PAR1, PAR2) -> RET, + method: HttpMethod, route: String? + ) { + if (method == HttpMethod.GET) + throw UnsupportedOperationException("GET method is only supported for methods without parameters") + val routeDef = route ?: "route${this::class.simpleName}${counter++}" + addRoute(method, "/kv/$routeDef") { req, ctx -> + val service = ctx.getBean(serviceClass.java) + initializeService(service, req) + val jsonRpcRequest = req.awaitBody() + if (jsonRpcRequest.params.size == 2) { + val param1 = getParameter(jsonRpcRequest.params[0]) + val param2 = getParameter(jsonRpcRequest.params[1]) + try { + val result = function.invoke(service, param1, param2) + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + result = mapper.writeValueAsString(result) + ) + ) + ) + } catch (e: Exception) { + if (e !is ServiceException) LOG.error(e.message, e) + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + error = e.message ?: "Error", + exceptionType = e.javaClass.canonicalName + ) + ) + ) + } + } else { + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + error = "Invalid parameters" + ) + ) + ) + } + } + } + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + @Suppress("TooGenericExceptionCaught") + protected actual inline fun bind( + noinline function: suspend T.(PAR1, PAR2, PAR3) -> RET, + method: HttpMethod, route: String? + ) { + if (method == HttpMethod.GET) + throw UnsupportedOperationException("GET method is only supported for methods without parameters") + val routeDef = route ?: "route${this::class.simpleName}${counter++}" + addRoute(method, "/kv/$routeDef") { req, ctx -> + val service = ctx.getBean(serviceClass.java) + initializeService(service, req) + val jsonRpcRequest = req.awaitBody() + @Suppress("MagicNumber") + if (jsonRpcRequest.params.size == 3) { + val param1 = getParameter(jsonRpcRequest.params[0]) + val param2 = getParameter(jsonRpcRequest.params[1]) + val param3 = getParameter(jsonRpcRequest.params[2]) + try { + val result = function.invoke(service, param1, param2, param3) + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + result = mapper.writeValueAsString(result) + ) + ) + ) + } catch (e: Exception) { + if (e !is ServiceException) LOG.error(e.message, e) + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + error = e.message ?: "Error", + exceptionType = e.javaClass.canonicalName + ) + ) + ) + } + } else { + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + error = "Invalid parameters" + ) + ) + ) + } + } + } + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + @Suppress("TooGenericExceptionCaught") + protected actual inline fun bind( + noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4) -> RET, + method: HttpMethod, route: String? + ) { + if (method == HttpMethod.GET) + throw UnsupportedOperationException("GET method is only supported for methods without parameters") + val routeDef = route ?: "route${this::class.simpleName}${counter++}" + addRoute(method, "/kv/$routeDef") { req, ctx -> + val service = ctx.getBean(serviceClass.java) + initializeService(service, req) + val jsonRpcRequest = req.awaitBody() + @Suppress("MagicNumber") + if (jsonRpcRequest.params.size == 4) { + val param1 = getParameter(jsonRpcRequest.params[0]) + val param2 = getParameter(jsonRpcRequest.params[1]) + val param3 = getParameter(jsonRpcRequest.params[2]) + val param4 = getParameter(jsonRpcRequest.params[3]) + try { + val result = function.invoke(service, param1, param2, param3, param4) + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + result = mapper.writeValueAsString(result) + ) + ) + ) + } catch (e: Exception) { + if (e !is ServiceException) LOG.error(e.message, e) + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + error = e.message ?: "Error", + exceptionType = e.javaClass.canonicalName + ) + ) + ) + } + } else { + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + error = "Invalid parameters" + ) + ) + ) + } + } + } + + /** + * Binds a given route with a function of the receiver. + * @param function a function of the receiver + * @param method a HTTP method + * @param route a route + */ + @Suppress("TooGenericExceptionCaught") + protected actual inline fun bind( + noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4, PAR5) -> RET, + method: HttpMethod, route: String? + ) { + if (method == HttpMethod.GET) + throw UnsupportedOperationException("GET method is only supported for methods without parameters") + val routeDef = route ?: "route${this::class.simpleName}${counter++}" + addRoute(method, "/kv/$routeDef") { req, ctx -> + val service = ctx.getBean(serviceClass.java) + initializeService(service, req) + val jsonRpcRequest = req.awaitBody() + @Suppress("MagicNumber") + if (jsonRpcRequest.params.size == 5) { + val param1 = getParameter(jsonRpcRequest.params[0]) + val param2 = getParameter(jsonRpcRequest.params[1]) + val param3 = getParameter(jsonRpcRequest.params[2]) + val param4 = getParameter(jsonRpcRequest.params[3]) + val param5 = getParameter(jsonRpcRequest.params[4]) + try { + val result = function.invoke(service, param1, param2, param3, param4, param5) + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + result = mapper.writeValueAsString(result) + ) + ) + ) + } catch (e: Exception) { + if (e !is ServiceException) LOG.error(e.message, e) + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + error = e.message ?: "Error", + exceptionType = e.javaClass.canonicalName + ) + ) + ) + } + } else { + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + error = "Invalid parameters" + ) + ) + ) + } + } + } + + /** + * Binds a given web socket connetion with a function of the receiver. + * @param function a function of the receiver + * @param route a route + */ + protected actual inline fun bind( + noinline function: suspend T.(ReceiveChannel, SendChannel) -> Unit, + route: String? + ) { + val routeDef = "route${this::class.simpleName}${counter++}" + webSocketsRequests[routeDef] = { webSocketSession, ctx, incoming, outgoing -> + val service = ctx.getBean(serviceClass.java) + if (service is WithWebSocketSession) { + service.webSocketSession = webSocketSession + } + if (service is WithPrincipal) { + val principal = webSocketSession.handshakeInfo.principal.awaitSingle() + service.principal = principal + } + val requestChannel = Channel() + val responseChannel = Channel() + coroutineScope { + launch { + for (p in incoming) { + val jsonRpcRequest = getParameter(p) + if (jsonRpcRequest.params.size == 1) { + val par = getParameter(jsonRpcRequest.params[0]) + requestChannel.send(par) + } + } + requestChannel.close() + } + launch { + for (p in responseChannel) { + val text = mapper.writeValueAsString( + JsonRpcResponse( + id = 0, + result = mapper.writeValueAsString(p) + ) + ) + outgoing.send(text) + } + if (!incoming.isClosedForReceive) incoming.cancel() + } + launch(start = CoroutineStart.UNDISPATCHED) { + function.invoke(service, requestChannel, responseChannel) + if (!responseChannel.isClosedForReceive) responseChannel.close() + } + } + } + } + + /** + * Binds a given function of the receiver as a tabulator component source + * @param function a function of the receiver + */ + @Suppress("TooGenericExceptionCaught") + protected actual inline fun bindTabulatorRemote( + noinline function: suspend T.(Int?, Int?, List?, List?, String?) -> RemoteData + ) { + val routeDef = "route${this::class.simpleName}${counter++}" + addRoute(HttpMethod.POST, "/kv/$routeDef") { req, ctx -> + val service = ctx.getBean(serviceClass.java) + initializeService(service, req) + val jsonRpcRequest = req.awaitBody() + @Suppress("MagicNumber") + if (jsonRpcRequest.params.size == 5) { + val param1 = getParameter(jsonRpcRequest.params[0]) + val param2 = getParameter(jsonRpcRequest.params[1]) + val param3 = getParameter?>(jsonRpcRequest.params[2]) + @Suppress("MagicNumber") + val param4 = getParameter?>(jsonRpcRequest.params[3]) + @Suppress("MagicNumber") + val param5 = getParameter(jsonRpcRequest.params[4]) + try { + val result = function.invoke(service, param1, param2, param3, param4, param5) + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + result = mapper.writeValueAsString(result) + ) + ) + ) + } catch (e: Exception) { + if (e !is ServiceException) LOG.error(e.message, e) + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + error = e.message ?: "Error", + exceptionType = e.javaClass.canonicalName + ) + ) + ) + } + } else { + ServerResponse.ok().json().bodyValueAndAwait( + mapper.writeValueAsString( + JsonRpcResponse( + id = jsonRpcRequest.id, + error = "Invalid parameters" + ) + ) + ) + } + } + } + + /** + * @suppress internal function + */ + fun addRoute( + method: HttpMethod, + path: String, + handler: suspend (ServerRequest, ApplicationContext) -> ServerResponse + ) { + when (method) { + HttpMethod.GET -> getRequests[path] = handler + HttpMethod.POST -> postRequests[path] = handler + HttpMethod.PUT -> putRequests[path] = handler + HttpMethod.DELETE -> deleteRequests[path] = handler + HttpMethod.OPTIONS -> optionsRequests[path] = handler + } + } + + /** + * @suppress internal function + */ + protected inline fun getParameter(str: String?): T { + return str?.let { + if (T::class == String::class) { + str as T + } else { + mapper.readValue(str) + } + } ?: null as T + } +} diff --git a/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVWebSocketConfig.kt b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVWebSocketConfig.kt new file mode 100644 index 00000000..3c0e4e99 --- /dev/null +++ b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVWebSocketConfig.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package pl.treksoft.kvision.remote + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactor.asFlux +import kotlinx.coroutines.reactor.asMono +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.reactive.HandlerMapping +import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping +import org.springframework.web.reactive.socket.WebSocketHandler +import org.springframework.web.reactive.socket.WebSocketSession +import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter +import reactor.core.publisher.Mono +import kotlin.coroutines.EmptyCoroutineContext + +class KVWebSocketHandler( + private val services: List>, + private val applicationContext: ApplicationContext +) : WebSocketHandler, CoroutineScope by CoroutineScope(Dispatchers.Default) { + + private fun getHandler(session: WebSocketSession): (suspend ( + WebSocketSession, ApplicationContext, + ReceiveChannel, SendChannel + ) -> Unit) { + val uri = session.handshakeInfo.uri.toString() + val route = uri.substring(uri.lastIndexOf('/') + 1) + return services.mapNotNull { + it.webSocketsRequests[route] + }.first() + } + + @OptIn( + ExperimentalCoroutinesApi::class, FlowPreview::class + ) + override fun handle(session: WebSocketSession): Mono { + val handler = getHandler(session) + val responseChannel = Channel() + val requestChannel = Channel() + val output = session.send(responseChannel.consumeAsFlow().asFlux().map(session::textMessage)) + val input = async { + coroutineScope { + launch { + session.receive().map { + it.payloadAsText + }.asFlow().collect { + requestChannel.send(it) + } + requestChannel.close() + } + launch { + handler.invoke(session, applicationContext, requestChannel, responseChannel) + if (!responseChannel.isClosedForReceive) responseChannel.close() + session.close() + } + } + }.asMono(EmptyCoroutineContext).then() + return Mono.zip(input, output).then() + } +} + +@Configuration +open class KVWebSocketConfig( + private var services: List>, + private var applicationContext: ApplicationContext +) { + + @Bean + open fun handlerMapping(): HandlerMapping { + val map = mapOf("/kvws/*" to KVWebSocketHandler(services, applicationContext)) + val order = -1 + return SimpleUrlHandlerMapping(map, order) + } + + @Bean + open fun handlerAdapter() = WebSocketHandlerAdapter() +} diff --git a/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/Security.kt b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/Security.kt new file mode 100644 index 00000000..7a9084dc --- /dev/null +++ b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/Security.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package pl.treksoft.kvision.remote + +import org.springframework.http.HttpMethod +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers + +/** + * A function to gather paths for spring security matchers. + */ +@Suppress("SpreadOperator", "MaxLineLength") +fun ServerHttpSecurity.AuthorizeExchangeSpec.serviceMatchers(vararg services: KVServiceManager<*>): ServerHttpSecurity.AuthorizeExchangeSpec.Access { + return this.matchers(*getServerWebExchangeMatcher(*services)) +} + +/** + * A function to gather paths for spring security matchers. + */ +@Suppress("SpreadOperator") +fun serviceMatchers(vararg services: KVServiceManager<*>): ServerWebExchangeMatcher { + return ServerWebExchangeMatchers.matchers(*getServerWebExchangeMatcher(*services)) +} + +/** + * A function to gather paths for spring security matchers. + */ +@Suppress("SpreadOperator") +fun getServerWebExchangeMatcher(vararg services: KVServiceManager<*>): Array { + val matchers = mutableListOf() + val getPaths = services.flatMap { it.getRequests.keys }.toTypedArray() + if (getPaths.isNotEmpty()) matchers.add(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, *getPaths)) + val postPaths = services.flatMap { it.postRequests.keys }.toTypedArray() + if (postPaths.isNotEmpty()) matchers.add(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, *postPaths)) + val putPaths = services.flatMap { it.putRequests.keys }.toTypedArray() + if (putPaths.isNotEmpty()) matchers.add(ServerWebExchangeMatchers.pathMatchers(HttpMethod.PUT, *putPaths)) + val deletePaths = services.flatMap { it.deleteRequests.keys }.toTypedArray() + if (deletePaths.isNotEmpty()) matchers.add(ServerWebExchangeMatchers.pathMatchers(HttpMethod.DELETE, *deletePaths)) + val optionsPaths = services.flatMap { it.optionsRequests.keys }.toTypedArray() + if (optionsPaths.isNotEmpty()) matchers.add( + ServerWebExchangeMatchers.pathMatchers( + HttpMethod.OPTIONS, + *optionsPaths + ) + ) + return matchers.toTypedArray() +} diff --git a/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/SessionInterfaces.kt b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/SessionInterfaces.kt new file mode 100644 index 00000000..63c5a9d1 --- /dev/null +++ b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/SessionInterfaces.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package pl.treksoft.kvision.remote + +import org.springframework.web.reactive.function.server.ServerRequest +import org.springframework.web.reactive.socket.WebSocketSession +import org.springframework.web.server.WebSession +import java.security.Principal + +interface WithRequest { + var serverRequest: ServerRequest +} + +interface WithWebSession { + var webSession: WebSession +} + +interface WithPrincipal { + var principal: Principal +} + +interface WithWebSocketSession { + var webSocketSession: WebSocketSession +} diff --git a/kvision-modules/kvision-server-spring-boot/src/jvmMain/resources/META-INF/spring.factories b/kvision-modules/kvision-server-spring-boot/src/jvmMain/resources/META-INF/spring.factories new file mode 100644 index 00000000..01084666 --- /dev/null +++ b/kvision-modules/kvision-server-spring-boot/src/jvmMain/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=pl.treksoft.kvision.remote.KVRouterConfiguration,pl.treksoft.kvision.remote.KVHandler,pl.treksoft.kvision.remote.KVWebSocketConfig diff --git a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/Annotations.kt b/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/Annotations.kt deleted file mode 100644 index 7de82631..00000000 --- a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/Annotations.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2017-present Robert Jaros - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package pl.treksoft.kvision.remote - -import org.springframework.data.annotation.Id -import org.springframework.data.annotation.Transient - -actual typealias Id = Id - -actual typealias Transient = Transient diff --git a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVRouterConfiguration.kt b/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVRouterConfiguration.kt deleted file mode 100644 index 42c4107d..00000000 --- a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVRouterConfiguration.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2017-present Robert Jaros - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package pl.treksoft.kvision.remote - -import org.springframework.beans.factory.annotation.Value -import org.springframework.context.ApplicationContext -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.support.GenericApplicationContext -import org.springframework.core.io.Resource -import org.springframework.http.MediaType.TEXT_HTML -import org.springframework.stereotype.Component -import org.springframework.web.reactive.function.server.ServerRequest -import org.springframework.web.reactive.function.server.ServerResponse -import org.springframework.web.reactive.function.server.buildAndAwait -import org.springframework.web.reactive.function.server.coRouter -import org.springframework.web.reactive.function.server.router - -@Configuration -open class KVRouterConfiguration { - @Value("classpath:/public/index.html") - private lateinit var indexHtml: Resource - - @Bean - open fun kvRoutes(kvHandler: KVHandler) = coRouter { - GET("/kv/**", kvHandler::handle) - POST("/kv/**", kvHandler::handle) - PUT("/kv/**", kvHandler::handle) - DELETE("/kv/**", kvHandler::handle) - OPTIONS("/kv/**", kvHandler::handle) - } - - @Bean - open fun indexRouter() = router { - GET("/") { - ok().contentType(TEXT_HTML).bodyValue(indexHtml) - } - } -} - -@Component -open class KVHandler(var services: List>, var applicationContext: ApplicationContext) { - - open suspend fun handle(request: ServerRequest): ServerResponse { - val routeUrl = request.path() - val handler = services.mapNotNull { - when (request.method()?.name) { - "GET" -> it.getRequests[routeUrl] - "POST" -> it.postRequests[routeUrl] - "PUT" -> it.putRequests[routeUrl] - "DELETE" -> it.deleteRequests[routeUrl] - "OPTIONS" -> it.optionsRequests[routeUrl] - else -> null - } - }.firstOrNull() - return if (handler != null) { - handler(request, applicationContext as GenericApplicationContext) - } else { - ServerResponse.notFound().buildAndAwait() - } - } -} diff --git a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt b/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt deleted file mode 100644 index 2ee939ef..00000000 --- a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt +++ /dev/null @@ -1,586 +0,0 @@ -/* - * Copyright (c) 2017-present Robert Jaros - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package pl.treksoft.kvision.remote - -import com.fasterxml.jackson.databind.module.SimpleModule -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.reactive.awaitSingle -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.springframework.context.ApplicationContext -import org.springframework.web.reactive.function.server.ServerRequest -import org.springframework.web.reactive.function.server.ServerResponse -import org.springframework.web.reactive.function.server.awaitBody -import org.springframework.web.reactive.function.server.bodyValueAndAwait -import org.springframework.web.reactive.function.server.json -import org.springframework.web.reactive.socket.WebSocketSession -import pl.treksoft.kvision.types.* -import java.math.BigDecimal -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalTime -import java.time.OffsetDateTime -import java.time.OffsetTime -import kotlin.reflect.KClass - -/** - * Multiplatform service manager for Spring Boot. - */ -@UseExperimental(ExperimentalCoroutinesApi::class) -@Suppress("LargeClass", "TooManyFunctions") -actual open class KVServiceManager actual constructor(val serviceClass: KClass) { - - companion object { - val LOG: Logger = LoggerFactory.getLogger(KVServiceManager::class.java.name) - } - - val getRequests: MutableMap ServerResponse> = - mutableMapOf() - val postRequests: MutableMap ServerResponse> = - mutableMapOf() - val putRequests: MutableMap ServerResponse> = - mutableMapOf() - val deleteRequests: MutableMap ServerResponse> = - mutableMapOf() - val optionsRequests: MutableMap ServerResponse> = - mutableMapOf() - val webSocketsRequests: MutableMap, SendChannel - ) -> Unit> = - mutableMapOf() - - val mapper = jacksonObjectMapper().apply { - val module = SimpleModule() - module.addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer()) - module.addSerializer(LocalDate::class.java, LocalDateSerializer()) - module.addSerializer(LocalTime::class.java, LocalTimeSerializer()) - module.addSerializer(OffsetDateTime::class.java, OffsetDateTimeSerializer()) - module.addSerializer(OffsetTime::class.java, OffsetTimeSerializer()) - module.addSerializer(BigDecimal::class.java, BigDecimalSerializer()) - module.addDeserializer(LocalDateTime::class.java, LocalDateTimeDeserializer()) - module.addDeserializer(LocalDate::class.java, LocalDateDeserializer()) - module.addDeserializer(LocalTime::class.java, LocalTimeDeserializer()) - module.addDeserializer(OffsetDateTime::class.java, OffsetDateTimeDeserializer()) - module.addDeserializer(OffsetTime::class.java, OffsetTimeDeserializer()) - module.addDeserializer(BigDecimal::class.java, BigDecimalDeserializer()) - this.registerModule(module) - } - var counter: Int = 0 - - /** - * @suppress internal function - */ - suspend fun initializeService(service: T, req: ServerRequest) { - if (service is WithRequest) { - service.serverRequest = req - } - if (service is WithWebSession) { - val session = req.session().awaitSingle() - service.webSession = session - } - if (service is WithPrincipal) { - val principal = req.principal().awaitSingle() - service.principal = principal - } - } - - /** - * Binds a given route with a function of the receiver. - * @param function a function of the receiver - * @param method a HTTP method - * @param route a route - */ - @Suppress("TooGenericExceptionCaught") - protected actual inline fun bind( - noinline function: suspend T.() -> RET, - method: HttpMethod, route: String? - ) { - val routeDef = route ?: "route${this::class.simpleName}${counter++}" - addRoute(method, "/kv/$routeDef") { req, ctx -> - val service = ctx.getBean(serviceClass.java) - initializeService(service, req) - val jsonRpcRequest = if (method == HttpMethod.GET) { - JsonRpcRequest(req.queryParam("id").orElse(null)?.toInt() ?: 0, "", listOf()) - } else { - req.awaitBody() - } - try { - val result = function.invoke(service) - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - result = mapper.writeValueAsString(result) - ) - ) - ) - } catch (e: Exception) { - if (e !is ServiceException) LOG.error(e.message, e) - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - error = e.message ?: "Error", - exceptionType = e.javaClass.canonicalName - ) - ) - ) - } - } - } - - /** - * Binds a given route with a function of the receiver. - * @param function a function of the receiver - * @param method a HTTP method - * @param route a route - */ - @Suppress("TooGenericExceptionCaught") - protected actual inline fun bind( - noinline function: suspend T.(PAR) -> RET, - method: HttpMethod, route: String? - ) { - if (method == HttpMethod.GET) - throw UnsupportedOperationException("GET method is only supported for methods without parameters") - val routeDef = route ?: "route${this::class.simpleName}${counter++}" - addRoute(method, "/kv/$routeDef") { req, ctx -> - val service = ctx.getBean(serviceClass.java) - initializeService(service, req) - val jsonRpcRequest = req.awaitBody() - if (jsonRpcRequest.params.size == 1) { - val param = getParameter(jsonRpcRequest.params[0]) - try { - val result = function.invoke(service, param) - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - result = mapper.writeValueAsString(result) - ) - ) - ) - } catch (e: Exception) { - if (e !is ServiceException) LOG.error(e.message, e) - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - error = e.message ?: "Error", - exceptionType = e.javaClass.canonicalName - ) - ) - ) - } - } else { - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - error = "Invalid parameters" - ) - ) - ) - } - } - } - - /** - * Binds a given route with a function of the receiver. - * @param function a function of the receiver - * @param method a HTTP method - * @param route a route - */ - @Suppress("TooGenericExceptionCaught") - protected actual inline fun bind( - noinline function: suspend T.(PAR1, PAR2) -> RET, - method: HttpMethod, route: String? - ) { - if (method == HttpMethod.GET) - throw UnsupportedOperationException("GET method is only supported for methods without parameters") - val routeDef = route ?: "route${this::class.simpleName}${counter++}" - addRoute(method, "/kv/$routeDef") { req, ctx -> - val service = ctx.getBean(serviceClass.java) - initializeService(service, req) - val jsonRpcRequest = req.awaitBody() - if (jsonRpcRequest.params.size == 2) { - val param1 = getParameter(jsonRpcRequest.params[0]) - val param2 = getParameter(jsonRpcRequest.params[1]) - try { - val result = function.invoke(service, param1, param2) - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - result = mapper.writeValueAsString(result) - ) - ) - ) - } catch (e: Exception) { - if (e !is ServiceException) LOG.error(e.message, e) - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - error = e.message ?: "Error", - exceptionType = e.javaClass.canonicalName - ) - ) - ) - } - } else { - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - error = "Invalid parameters" - ) - ) - ) - } - } - } - - /** - * Binds a given route with a function of the receiver. - * @param function a function of the receiver - * @param method a HTTP method - * @param route a route - */ - @Suppress("TooGenericExceptionCaught") - protected actual inline fun bind( - noinline function: suspend T.(PAR1, PAR2, PAR3) -> RET, - method: HttpMethod, route: String? - ) { - if (method == HttpMethod.GET) - throw UnsupportedOperationException("GET method is only supported for methods without parameters") - val routeDef = route ?: "route${this::class.simpleName}${counter++}" - addRoute(method, "/kv/$routeDef") { req, ctx -> - val service = ctx.getBean(serviceClass.java) - initializeService(service, req) - val jsonRpcRequest = req.awaitBody() - @Suppress("MagicNumber") - if (jsonRpcRequest.params.size == 3) { - val param1 = getParameter(jsonRpcRequest.params[0]) - val param2 = getParameter(jsonRpcRequest.params[1]) - val param3 = getParameter(jsonRpcRequest.params[2]) - try { - val result = function.invoke(service, param1, param2, param3) - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - result = mapper.writeValueAsString(result) - ) - ) - ) - } catch (e: Exception) { - if (e !is ServiceException) LOG.error(e.message, e) - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - error = e.message ?: "Error", - exceptionType = e.javaClass.canonicalName - ) - ) - ) - } - } else { - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - error = "Invalid parameters" - ) - ) - ) - } - } - } - - /** - * Binds a given route with a function of the receiver. - * @param function a function of the receiver - * @param method a HTTP method - * @param route a route - */ - @Suppress("TooGenericExceptionCaught") - protected actual inline fun bind( - noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4) -> RET, - method: HttpMethod, route: String? - ) { - if (method == HttpMethod.GET) - throw UnsupportedOperationException("GET method is only supported for methods without parameters") - val routeDef = route ?: "route${this::class.simpleName}${counter++}" - addRoute(method, "/kv/$routeDef") { req, ctx -> - val service = ctx.getBean(serviceClass.java) - initializeService(service, req) - val jsonRpcRequest = req.awaitBody() - @Suppress("MagicNumber") - if (jsonRpcRequest.params.size == 4) { - val param1 = getParameter(jsonRpcRequest.params[0]) - val param2 = getParameter(jsonRpcRequest.params[1]) - val param3 = getParameter(jsonRpcRequest.params[2]) - val param4 = getParameter(jsonRpcRequest.params[3]) - try { - val result = function.invoke(service, param1, param2, param3, param4) - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - result = mapper.writeValueAsString(result) - ) - ) - ) - } catch (e: Exception) { - if (e !is ServiceException) LOG.error(e.message, e) - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - error = e.message ?: "Error", - exceptionType = e.javaClass.canonicalName - ) - ) - ) - } - } else { - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - error = "Invalid parameters" - ) - ) - ) - } - } - } - - /** - * Binds a given route with a function of the receiver. - * @param function a function of the receiver - * @param method a HTTP method - * @param route a route - */ - @Suppress("TooGenericExceptionCaught") - protected actual inline fun bind( - noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4, PAR5) -> RET, - method: HttpMethod, route: String? - ) { - if (method == HttpMethod.GET) - throw UnsupportedOperationException("GET method is only supported for methods without parameters") - val routeDef = route ?: "route${this::class.simpleName}${counter++}" - addRoute(method, "/kv/$routeDef") { req, ctx -> - val service = ctx.getBean(serviceClass.java) - initializeService(service, req) - val jsonRpcRequest = req.awaitBody() - @Suppress("MagicNumber") - if (jsonRpcRequest.params.size == 5) { - val param1 = getParameter(jsonRpcRequest.params[0]) - val param2 = getParameter(jsonRpcRequest.params[1]) - val param3 = getParameter(jsonRpcRequest.params[2]) - val param4 = getParameter(jsonRpcRequest.params[3]) - val param5 = getParameter(jsonRpcRequest.params[4]) - try { - val result = function.invoke(service, param1, param2, param3, param4, param5) - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - result = mapper.writeValueAsString(result) - ) - ) - ) - } catch (e: Exception) { - if (e !is ServiceException) LOG.error(e.message, e) - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - error = e.message ?: "Error", - exceptionType = e.javaClass.canonicalName - ) - ) - ) - } - } else { - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - error = "Invalid parameters" - ) - ) - ) - } - } - } - - /** - * Binds a given web socket connetion with a function of the receiver. - * @param function a function of the receiver - * @param route a route - */ - protected actual inline fun bind( - noinline function: suspend T.(ReceiveChannel, SendChannel) -> Unit, - route: String? - ) { - val routeDef = "route${this::class.simpleName}${counter++}" - webSocketsRequests[routeDef] = { webSocketSession, ctx, incoming, outgoing -> - val service = ctx.getBean(serviceClass.java) - if (service is WithWebSocketSession) { - service.webSocketSession = webSocketSession - } - if (service is WithPrincipal) { - val principal = webSocketSession.handshakeInfo.principal.awaitSingle() - service.principal = principal - } - val requestChannel = Channel() - val responseChannel = Channel() - coroutineScope { - launch { - for (p in incoming) { - val jsonRpcRequest = getParameter(p) - if (jsonRpcRequest.params.size == 1) { - val par = getParameter(jsonRpcRequest.params[0]) - requestChannel.send(par) - } - } - requestChannel.close() - } - launch { - for (p in responseChannel) { - val text = mapper.writeValueAsString( - JsonRpcResponse( - id = 0, - result = mapper.writeValueAsString(p) - ) - ) - outgoing.send(text) - } - if (!incoming.isClosedForReceive) incoming.cancel() - } - launch(start = CoroutineStart.UNDISPATCHED) { - function.invoke(service, requestChannel, responseChannel) - if (!responseChannel.isClosedForReceive) responseChannel.close() - } - } - } - } - - /** - * Binds a given function of the receiver as a tabulator component source - * @param function a function of the receiver - */ - @Suppress("TooGenericExceptionCaught") - protected actual inline fun bindTabulatorRemote( - noinline function: suspend T.(Int?, Int?, List?, List?, String?) -> RemoteData - ) { - val routeDef = "route${this::class.simpleName}${counter++}" - addRoute(HttpMethod.POST, "/kv/$routeDef") { req, ctx -> - val service = ctx.getBean(serviceClass.java) - initializeService(service, req) - val jsonRpcRequest = req.awaitBody() - @Suppress("MagicNumber") - if (jsonRpcRequest.params.size == 5) { - val param1 = getParameter(jsonRpcRequest.params[0]) - val param2 = getParameter(jsonRpcRequest.params[1]) - val param3 = getParameter?>(jsonRpcRequest.params[2]) - @Suppress("MagicNumber") - val param4 = getParameter?>(jsonRpcRequest.params[3]) - @Suppress("MagicNumber") - val param5 = getParameter(jsonRpcRequest.params[4]) - try { - val result = function.invoke(service, param1, param2, param3, param4, param5) - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - result = mapper.writeValueAsString(result) - ) - ) - ) - } catch (e: Exception) { - if (e !is ServiceException) LOG.error(e.message, e) - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - error = e.message ?: "Error", - exceptionType = e.javaClass.canonicalName - ) - ) - ) - } - } else { - ServerResponse.ok().json().bodyValueAndAwait( - mapper.writeValueAsString( - JsonRpcResponse( - id = jsonRpcRequest.id, - error = "Invalid parameters" - ) - ) - ) - } - } - } - - /** - * @suppress internal function - */ - fun addRoute( - method: HttpMethod, - path: String, - handler: suspend (ServerRequest, ApplicationContext) -> ServerResponse - ) { - when (method) { - HttpMethod.GET -> getRequests[path] = handler - HttpMethod.POST -> postRequests[path] = handler - HttpMethod.PUT -> putRequests[path] = handler - HttpMethod.DELETE -> deleteRequests[path] = handler - HttpMethod.OPTIONS -> optionsRequests[path] = handler - } - } - - /** - * @suppress internal function - */ - protected inline fun getParameter(str: String?): T { - return str?.let { - if (T::class == String::class) { - str as T - } else { - mapper.readValue(str) - } - } ?: null as T - } -} diff --git a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVWebSocketConfig.kt b/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVWebSocketConfig.kt deleted file mode 100644 index 2c74e611..00000000 --- a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVWebSocketConfig.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2017-present Robert Jaros - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package pl.treksoft.kvision.remote - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.async -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.consumeAsFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.reactive.asFlow -import kotlinx.coroutines.reactor.asFlux -import kotlinx.coroutines.reactor.asMono -import org.springframework.context.ApplicationContext -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.web.reactive.HandlerMapping -import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping -import org.springframework.web.reactive.socket.WebSocketHandler -import org.springframework.web.reactive.socket.WebSocketSession -import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter -import reactor.core.publisher.Mono -import kotlin.coroutines.EmptyCoroutineContext - -class KVWebSocketHandler( - private val services: List>, - private val applicationContext: ApplicationContext -) : WebSocketHandler, CoroutineScope by CoroutineScope(Dispatchers.Default) { - - private fun getHandler(session: WebSocketSession): (suspend ( - WebSocketSession, ApplicationContext, - ReceiveChannel, SendChannel - ) -> Unit) { - val uri = session.handshakeInfo.uri.toString() - val route = uri.substring(uri.lastIndexOf('/') + 1) - return services.mapNotNull { - it.webSocketsRequests[route] - }.first() - } - - @UseExperimental(ExperimentalCoroutinesApi::class, FlowPreview::class) - override fun handle(session: WebSocketSession): Mono { - val handler = getHandler(session) - val responseChannel = Channel() - val requestChannel = Channel() - val output = session.send(responseChannel.consumeAsFlow().asFlux().map(session::textMessage)) - val input = async { - coroutineScope { - launch { - session.receive().map { - it.payloadAsText - }.asFlow().collect { - requestChannel.send(it) - } - requestChannel.close() - } - launch { - handler.invoke(session, applicationContext, requestChannel, responseChannel) - if (!responseChannel.isClosedForReceive) responseChannel.close() - session.close() - } - } - }.asMono(EmptyCoroutineContext).then() - return Mono.zip(input, output).then() - } -} - -@Configuration -open class KVWebSocketConfig( - private var services: List>, - private var applicationContext: ApplicationContext -) { - - @Bean - open fun handlerMapping(): HandlerMapping { - val map = mapOf("/kvws/*" to KVWebSocketHandler(services, applicationContext)) - val order = -1 - return SimpleUrlHandlerMapping(map, order) - } - - @Bean - open fun handlerAdapter() = WebSocketHandlerAdapter() -} diff --git a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/Security.kt b/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/Security.kt deleted file mode 100644 index 7a9084dc..00000000 --- a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/Security.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2017-present Robert Jaros - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package pl.treksoft.kvision.remote - -import org.springframework.http.HttpMethod -import org.springframework.security.config.web.server.ServerHttpSecurity -import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher -import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers - -/** - * A function to gather paths for spring security matchers. - */ -@Suppress("SpreadOperator", "MaxLineLength") -fun ServerHttpSecurity.AuthorizeExchangeSpec.serviceMatchers(vararg services: KVServiceManager<*>): ServerHttpSecurity.AuthorizeExchangeSpec.Access { - return this.matchers(*getServerWebExchangeMatcher(*services)) -} - -/** - * A function to gather paths for spring security matchers. - */ -@Suppress("SpreadOperator") -fun serviceMatchers(vararg services: KVServiceManager<*>): ServerWebExchangeMatcher { - return ServerWebExchangeMatchers.matchers(*getServerWebExchangeMatcher(*services)) -} - -/** - * A function to gather paths for spring security matchers. - */ -@Suppress("SpreadOperator") -fun getServerWebExchangeMatcher(vararg services: KVServiceManager<*>): Array { - val matchers = mutableListOf() - val getPaths = services.flatMap { it.getRequests.keys }.toTypedArray() - if (getPaths.isNotEmpty()) matchers.add(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, *getPaths)) - val postPaths = services.flatMap { it.postRequests.keys }.toTypedArray() - if (postPaths.isNotEmpty()) matchers.add(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, *postPaths)) - val putPaths = services.flatMap { it.putRequests.keys }.toTypedArray() - if (putPaths.isNotEmpty()) matchers.add(ServerWebExchangeMatchers.pathMatchers(HttpMethod.PUT, *putPaths)) - val deletePaths = services.flatMap { it.deleteRequests.keys }.toTypedArray() - if (deletePaths.isNotEmpty()) matchers.add(ServerWebExchangeMatchers.pathMatchers(HttpMethod.DELETE, *deletePaths)) - val optionsPaths = services.flatMap { it.optionsRequests.keys }.toTypedArray() - if (optionsPaths.isNotEmpty()) matchers.add( - ServerWebExchangeMatchers.pathMatchers( - HttpMethod.OPTIONS, - *optionsPaths - ) - ) - return matchers.toTypedArray() -} diff --git a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/SessionInterfaces.kt b/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/SessionInterfaces.kt deleted file mode 100644 index 63c5a9d1..00000000 --- a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/SessionInterfaces.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2017-present Robert Jaros - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package pl.treksoft.kvision.remote - -import org.springframework.web.reactive.function.server.ServerRequest -import org.springframework.web.reactive.socket.WebSocketSession -import org.springframework.web.server.WebSession -import java.security.Principal - -interface WithRequest { - var serverRequest: ServerRequest -} - -interface WithWebSession { - var webSession: WebSession -} - -interface WithPrincipal { - var principal: Principal -} - -interface WithWebSocketSession { - var webSocketSession: WebSocketSession -} diff --git a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/types/Date.kt b/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/types/Date.kt deleted file mode 100644 index 61f8ba58..00000000 --- a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/types/Date.kt +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (c) 2017-present Robert Jaros - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package pl.treksoft.kvision.types - -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.JsonDeserializer -import com.fasterxml.jackson.databind.JsonSerializer -import com.fasterxml.jackson.databind.SerializerProvider -import java.io.IOException -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalTime -import java.time.OffsetDateTime -import java.time.OffsetTime -import java.time.format.DateTimeFormatter -import java.time.format.DateTimeParseException - -actual typealias LocalDateTime = LocalDateTime - -actual typealias LocalDate = LocalDate - -actual typealias LocalTime = LocalTime - -actual typealias OffsetDateTime = OffsetDateTime - -actual typealias OffsetTime = OffsetTime - -fun String.toDateTimeF(): LocalDateTime = LocalDateTime.parse(this) - -fun String.toDateF(): LocalDate = LocalDate.parse(this) - -fun String.toTimeF(): LocalTime = LocalTime.parse(this) - -fun String.toOffsetDateTimeF(): OffsetDateTime = OffsetDateTime.parse(this) - -fun String.toOffsetTimeF(): OffsetTime = OffsetTime.parse(this) - -fun LocalDateTime.toStringF(): String = this.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) - -fun LocalDate.toStringF(): String = this.format(DateTimeFormatter.ISO_LOCAL_DATE) - -fun LocalTime.toStringF(): String = this.format(DateTimeFormatter.ISO_LOCAL_TIME) - -fun OffsetDateTime.toStringF(): String = this.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) - -fun OffsetTime.toStringF(): String = this.format(DateTimeFormatter.ISO_OFFSET_TIME) - -class LocalDateTimeSerializer : JsonSerializer() { - @Throws(IOException::class) - override fun serialize(value: LocalDateTime, gen: JsonGenerator, provider: SerializerProvider) { - try { - val s = value.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) - gen.writeString(s) - } catch (e: DateTimeParseException) { - System.err.println(e) - gen.writeString("") - } - } -} - -class LocalDateTimeDeserializer : JsonDeserializer() { - @Throws(IOException::class) - override fun deserialize(p: JsonParser, ctx: DeserializationContext): LocalDateTime? { - val str = p.text - try { - @Suppress("MagicNumber") - return LocalDateTime.parse(str.dropLast(6), DateTimeFormatter.ISO_LOCAL_DATE_TIME) - } catch (e: DateTimeParseException) { - System.err.println(e) - return null - } - } -} - -class LocalDateSerializer : JsonSerializer() { - @Throws(IOException::class) - override fun serialize(value: LocalDate, gen: JsonGenerator, provider: SerializerProvider) { - try { - val s = value.atStartOfDay().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) - gen.writeString(s) - } catch (e: DateTimeParseException) { - System.err.println(e) - gen.writeString("") - } - } -} - -class LocalDateDeserializer : JsonDeserializer() { - @Throws(IOException::class) - override fun deserialize(p: JsonParser, ctx: DeserializationContext): LocalDate? { - val str = p.text - try { - @Suppress("MagicNumber") - return LocalDate.parse(str.dropLast(6), DateTimeFormatter.ISO_LOCAL_DATE_TIME) - } catch (e: DateTimeParseException) { - System.err.println(e) - return null - } - } -} - -class LocalTimeSerializer : JsonSerializer() { - @Throws(IOException::class) - override fun serialize(value: LocalTime, gen: JsonGenerator, provider: SerializerProvider) { - try { - val s = value.atDate(LocalDate.now()).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) - gen.writeString(s) - } catch (e: DateTimeParseException) { - System.err.println(e) - gen.writeString("") - } - } -} - -class LocalTimeDeserializer : JsonDeserializer() { - @Throws(IOException::class) - override fun deserialize(p: JsonParser, ctx: DeserializationContext): LocalTime? { - val str = p.text - try { - @Suppress("MagicNumber") - return LocalTime.parse(str.dropLast(6), DateTimeFormatter.ISO_LOCAL_DATE_TIME) - } catch (e: DateTimeParseException) { - System.err.println(e) - return null - } - } -} - -class OffsetDateTimeSerializer : JsonSerializer() { - @Throws(IOException::class) - override fun serialize(value: OffsetDateTime, gen: JsonGenerator, provider: SerializerProvider) { - try { - val s = value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) - gen.writeString(s) - } catch (e: DateTimeParseException) { - System.err.println(e) - gen.writeString("") - } - } -} - -class OffsetDateTimeDeserializer : JsonDeserializer() { - @Throws(IOException::class) - override fun deserialize(p: JsonParser, ctx: DeserializationContext): OffsetDateTime? { - val str = p.text - return try { - OffsetDateTime.parse(str, DateTimeFormatter.ISO_OFFSET_DATE_TIME) - } catch (e: DateTimeParseException) { - System.err.println(e) - null - } - } -} - -class OffsetTimeSerializer : JsonSerializer() { - @Throws(IOException::class) - override fun serialize(value: OffsetTime, gen: JsonGenerator, provider: SerializerProvider) { - try { - val s = value.atDate(LocalDate.now()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) - gen.writeString(s) - } catch (e: DateTimeParseException) { - System.err.println(e) - gen.writeString("") - } - } -} - -class OffsetTimeDeserializer : JsonDeserializer() { - @Throws(IOException::class) - override fun deserialize(p: JsonParser, ctx: DeserializationContext): OffsetTime? { - val str = p.text - return try { - OffsetTime.parse(str, DateTimeFormatter.ISO_OFFSET_DATE_TIME) - } catch (e: DateTimeParseException) { - System.err.println(e) - null - } - } -} diff --git a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/types/Decimal.kt b/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/types/Decimal.kt deleted file mode 100644 index 1d37ae51..00000000 --- a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/types/Decimal.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2017-present Robert Jaros - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package pl.treksoft.kvision.types - -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.JsonDeserializer -import com.fasterxml.jackson.databind.JsonSerializer -import com.fasterxml.jackson.databind.SerializerProvider -import java.io.IOException -import java.math.BigDecimal - -actual typealias Decimal = BigDecimal - -class BigDecimalSerializer : JsonSerializer() { - @Throws(IOException::class) - override fun serialize(value: BigDecimal, gen: JsonGenerator, provider: SerializerProvider) { - gen.writeNumber(value.toDouble()) - } -} - -class BigDecimalDeserializer : JsonDeserializer() { - @Throws(IOException::class) - override fun deserialize(p: JsonParser, ctx: DeserializationContext): BigDecimal? { - return p.doubleValue.toBigDecimal() - } -} diff --git a/kvision-modules/kvision-server-spring-boot/src/main/resources/META-INF/spring.factories b/kvision-modules/kvision-server-spring-boot/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 01084666..00000000 --- a/kvision-modules/kvision-server-spring-boot/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=pl.treksoft.kvision.remote.KVRouterConfiguration,pl.treksoft.kvision.remote.KVHandler,pl.treksoft.kvision.remote.KVWebSocketConfig -- cgit