aboutsummaryrefslogtreecommitdiff
path: root/kvision-modules
diff options
context:
space:
mode:
authorRobert Jaros <rjaros@finn.pl>2019-10-12 18:22:43 +0200
committerRobert Jaros <rjaros@finn.pl>2019-10-12 18:22:43 +0200
commitbcf7504392baccf1568e740c1d453eac7080fb5b (patch)
tree519489751032814bfc0d5426c48de257b788329f /kvision-modules
parent28b6e9d646123d774660ebdf0124eeb8a22fe087 (diff)
downloadkvision-bcf7504392baccf1568e740c1d453eac7080fb5b.tar.gz
kvision-bcf7504392baccf1568e740c1d453eac7080fb5b.tar.bz2
kvision-bcf7504392baccf1568e740c1d453eac7080fb5b.zip
Redesign spring-boot server module to use Spring WebFlux instead of Spring MVC.
Diffstat (limited to 'kvision-modules')
-rw-r--r--kvision-modules/kvision-common-remote/src/main/kotlin/pl/treksoft/kvision/remote/Annotations.kt38
-rw-r--r--kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt3
-rw-r--r--kvision-modules/kvision-server-jooby/src/main/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt23
-rw-r--r--kvision-modules/kvision-server-ktor/src/main/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt22
-rw-r--r--kvision-modules/kvision-server-ktor/src/main/kotlin/pl/treksoft/kvision/remote/Profile.kt8
-rw-r--r--kvision-modules/kvision-server-spring-boot/build.gradle16
-rw-r--r--kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/Annotations.kt35
-rw-r--r--kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVController.kt66
-rw-r--r--kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVRouterConfiguration.kt84
-rw-r--r--kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt369
-rw-r--r--kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVWebSocketConfig.kt263
-rw-r--r--kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/Profile.kt137
-rw-r--r--kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/Security.kt30
-rw-r--r--kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/SessionInterfaces.kt47
-rw-r--r--kvision-modules/kvision-server-spring-boot/src/main/resources/META-INF/spring.factories2
15 files changed, 665 insertions, 478 deletions
diff --git a/kvision-modules/kvision-common-remote/src/main/kotlin/pl/treksoft/kvision/remote/Annotations.kt b/kvision-modules/kvision-common-remote/src/main/kotlin/pl/treksoft/kvision/remote/Annotations.kt
new file mode 100644
index 00000000..997f0369
--- /dev/null
+++ b/kvision-modules/kvision-common-remote/src/main/kotlin/pl/treksoft/kvision/remote/Annotations.kt
@@ -0,0 +1,38 @@
+/*
+ * 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
+
+@UseExperimental(ExperimentalMultiplatform::class)
+@OptionalExpectation
+expect annotation class Id()
+
+@UseExperimental(ExperimentalMultiplatform::class)
+@OptionalExpectation
+expect annotation class Table()
+
+@UseExperimental(ExperimentalMultiplatform::class)
+@OptionalExpectation
+expect annotation class Column()
+
+@UseExperimental(ExperimentalMultiplatform::class)
+@OptionalExpectation
+expect annotation class Transient()
diff --git a/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt
index 06e150e4..1fb90507 100644
--- a/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt
+++ b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt
@@ -35,6 +35,7 @@ import kotlinx.serialization.internal.StringSerializer
import kotlinx.serialization.list
import kotlinx.serialization.serializer
import pl.treksoft.kvision.types.JsonDateSerializer
+import pl.treksoft.kvision.types.toStringInternal
import kotlin.js.Date
import kotlin.reflect.KClass
@@ -113,7 +114,7 @@ interface RemoteAgent {
is Enum<*> -> "\"$value\""
is String -> value
is Char -> "\"$value\""
- is Date -> "\"${value.getTime()}\""
+ is Date -> "\"${value.toStringInternal()}\""
else -> try {
@Suppress("UNCHECKED_CAST")
JSON.plain.stringify(kClass.serializer(), value)
diff --git a/kvision-modules/kvision-server-jooby/src/main/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt b/kvision-modules/kvision-server-jooby/src/main/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt
index 4c4d4af0..e2a26967 100644
--- a/kvision-modules/kvision-server-jooby/src/main/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt
+++ b/kvision-modules/kvision-server-jooby/src/main/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt
@@ -21,6 +21,7 @@
*/
package pl.treksoft.kvision.remote
+import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.google.inject.Injector
import kotlinx.coroutines.CoroutineStart
@@ -37,7 +38,14 @@ import org.jooby.Request
import org.jooby.Response
import org.slf4j.Logger
import org.slf4j.LoggerFactory
+import pl.treksoft.kvision.types.*
import kotlin.reflect.KClass
+import java.time.LocalDateTime
+import java.time.LocalDate
+import java.time.LocalTime
+import java.time.OffsetDateTime
+import java.time.OffsetTime
+
/**
* Multiplatform service manager for Jooby.
@@ -51,7 +59,20 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
}
val routes: MutableList<Kooby.() -> Unit> = mutableListOf()
- val mapper = jacksonObjectMapper()
+ 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.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())
+ this.registerModule(module)
+ }
var counter: Int = 0
/**
diff --git a/kvision-modules/kvision-server-ktor/src/main/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt b/kvision-modules/kvision-server-ktor/src/main/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt
index 7ccd6341..36a7897e 100644
--- a/kvision-modules/kvision-server-ktor/src/main/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt
+++ b/kvision-modules/kvision-server-ktor/src/main/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt
@@ -21,6 +21,7 @@
*/
package pl.treksoft.kvision.remote
+import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.ktor.application.ApplicationCall
import io.ktor.application.call
@@ -49,6 +50,12 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import org.slf4j.Logger
import org.slf4j.LoggerFactory
+import pl.treksoft.kvision.types.*
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
+import java.time.OffsetDateTime
+import java.time.OffsetTime
import kotlin.reflect.KClass
/**
@@ -73,7 +80,20 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
val webSocketRequests: MutableMap<String, suspend WebSocketServerSession.() -> Unit> =
mutableMapOf()
- val mapper = jacksonObjectMapper()
+ 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.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())
+ this.registerModule(module)
+ }
var counter: Int = 0
/**
diff --git a/kvision-modules/kvision-server-ktor/src/main/kotlin/pl/treksoft/kvision/remote/Profile.kt b/kvision-modules/kvision-server-ktor/src/main/kotlin/pl/treksoft/kvision/remote/Profile.kt
index 693c8e3c..cc77f341 100644
--- a/kvision-modules/kvision-server-ktor/src/main/kotlin/pl/treksoft/kvision/remote/Profile.kt
+++ b/kvision-modules/kvision-server-ktor/src/main/kotlin/pl/treksoft/kvision/remote/Profile.kt
@@ -25,7 +25,6 @@ import io.ktor.application.ApplicationCall
import io.ktor.sessions.get
import io.ktor.sessions.sessions
import kotlinx.serialization.Serializable
-import kotlinx.serialization.Transient
/**
* A user profile.
@@ -41,7 +40,6 @@ actual data class Profile(
val remembered: Boolean = false,
val clientName: String? = null
) {
- @Transient
var username: String?
get() = attributes["username"]
set(value) {
@@ -51,7 +49,6 @@ actual data class Profile(
attributes.remove("username")
}
}
- @Transient
var firstName: String?
get() = attributes["first_name"]
set(value) {
@@ -61,7 +58,6 @@ actual data class Profile(
attributes.remove("first_name")
}
}
- @Transient
var familyName: String?
get() = attributes["family_name"]
set(value) {
@@ -71,7 +67,6 @@ actual data class Profile(
attributes.remove("family_name")
}
}
- @Transient
var displayName: String?
get() = attributes["display_name"]
set(value) {
@@ -81,7 +76,6 @@ actual data class Profile(
attributes.remove("display_name")
}
}
- @Transient
var email: String?
get() = attributes["email"]
set(value) {
@@ -91,7 +85,6 @@ actual data class Profile(
attributes.remove("email")
}
}
- @Transient
var pictureUrl: String?
get() = attributes["picture_url"]
set(value) {
@@ -101,7 +94,6 @@ actual data class Profile(
attributes.remove("picture_url")
}
}
- @Transient
var profileUrl: String?
get() = attributes["profile_url"]
set(value) {
diff --git a/kvision-modules/kvision-server-spring-boot/build.gradle b/kvision-modules/kvision-server-spring-boot/build.gradle
index 6e79d007..e277547b 100644
--- a/kvision-modules/kvision-server-spring-boot/build.gradle
+++ b/kvision-modules/kvision-server-spring-boot/build.gradle
@@ -1,19 +1,29 @@
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")
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-web:$springBootVersion"
- compile "org.springframework.boot:spring-boot-starter-websocket:$springBootVersion"
- compile "org.pac4j:pac4j-core:$pac4jVersion"
+ 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")
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
new file mode 100644
index 00000000..7655ae70
--- /dev/null
+++ b/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/Annotations.kt
@@ -0,0 +1,35 @@
+/*
+ * 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
+import org.springframework.data.relational.core.mapping.Column
+import org.springframework.data.relational.core.mapping.Table
+
+actual typealias Id = Id
+
+actual typealias Transient = Transient
+
+actual typealias Table = Table
+
+actual typealias Column = Column
diff --git a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVController.kt b/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVController.kt
deleted file mode 100644
index 23e00284..00000000
--- a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVController.kt
+++ /dev/null
@@ -1,66 +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.Autowired
-import org.springframework.context.ApplicationContext
-import org.springframework.stereotype.Controller
-import org.springframework.web.bind.annotation.RequestMapping
-import org.springframework.web.bind.annotation.RequestMethod
-import javax.servlet.http.HttpServletRequest
-import javax.servlet.http.HttpServletResponse
-
-/**
- * Controller for handling automatic routes.
- */
-@Controller
-open class KVController {
-
- @Autowired
- lateinit var services: List<KVServiceManager<*>>
-
- @Autowired
- lateinit var applicationContext: ApplicationContext
-
- @RequestMapping(
- "/kv/**",
- method = [RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.OPTIONS]
- )
- open fun kVMapping(req: HttpServletRequest, res: HttpServletResponse) {
- val routeUrl = req.requestURI
- val handler = services.mapNotNull {
- when (req.method) {
- "GET" -> it.getRequests[routeUrl]
- "POST" -> it.postRequests[routeUrl]
- "PUT" -> it.putRequests[routeUrl]
- "DELETE" -> it.deleteRequests[routeUrl]
- "OPTIONS" -> it.optionsRequests[routeUrl]
- else -> null
- }
- }.firstOrNull()
- if (handler != null) {
- handler.invoke(req, res, applicationContext)
- } else {
- res.status = HttpServletResponse.SC_NOT_FOUND
- }
- }
-}
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
new file mode 100644
index 00000000..215848a7
--- /dev/null
+++ b/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/KVRouterConfiguration.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.support.GenericApplicationContext
+import org.springframework.stereotype.Component
+import org.springframework.web.reactive.function.server.RouterFunction
+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
+import java.net.URI
+
+@Configuration
+open class KVRouterConfiguration {
+ @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(): RouterFunction<ServerResponse> {
+ val redirectToIndex =
+ ServerResponse
+ .temporaryRedirect(URI("/index.html"))
+ .build()
+
+ return router {
+ GET("/") {
+ redirectToIndex
+ }
+ }
+ }
+}
+
+@Component
+open class KVHandler(var services: List<KVServiceManager<*>>, 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
index 185356da..14360abc 100644
--- 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
@@ -21,24 +21,35 @@
*/
package pl.treksoft.kvision.remote
+import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.GlobalScope
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.context.support.GenericWebApplicationContext
-import org.springframework.web.socket.WebSocketSession
-import javax.servlet.http.HttpServletRequest
-import javax.servlet.http.HttpServletResponse
+import org.springframework.security.core.Authentication
+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.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.
*/
@@ -50,25 +61,61 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
val LOG: Logger = LoggerFactory.getLogger(KVServiceManager::class.java.name)
}
- val getRequests: MutableMap<String, (HttpServletRequest, HttpServletResponse, ApplicationContext) -> Unit> =
+ val getRequests: MutableMap<String, suspend (ServerRequest, ApplicationContext) -> ServerResponse> =
mutableMapOf()
- val postRequests: MutableMap<String, (HttpServletRequest, HttpServletResponse, ApplicationContext) -> Unit> =
+ val postRequests: MutableMap<String, suspend (ServerRequest, ApplicationContext) -> ServerResponse> =
mutableMapOf()
- val putRequests: MutableMap<String, (HttpServletRequest, HttpServletResponse, ApplicationContext) -> Unit> =
+ val putRequests: MutableMap<String, suspend (ServerRequest, ApplicationContext) -> ServerResponse> =
mutableMapOf()
- val deleteRequests: MutableMap<String, (HttpServletRequest, HttpServletResponse, ApplicationContext) -> Unit> =
+ val deleteRequests: MutableMap<String, suspend (ServerRequest, ApplicationContext) -> ServerResponse> =
mutableMapOf()
- val optionsRequests: MutableMap<String, (HttpServletRequest, HttpServletResponse, ApplicationContext) -> Unit> =
+ val optionsRequests: MutableMap<String, suspend (ServerRequest, ApplicationContext) -> ServerResponse> =
mutableMapOf()
val webSocketsRequests: MutableMap<String, suspend (
- WebSocketSession, GenericWebApplicationContext, ReceiveChannel<String>, SendChannel<String>
+ WebSocketSession, ApplicationContext, ReceiveChannel<String>, SendChannel<String>
) -> Unit> =
mutableMapOf()
- val mapper = jacksonObjectMapper()
+ 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.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())
+ 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
+ }
+ if (service is WithProfile) {
+ val profile = req.principal().ofType(Authentication::class.java).map {
+ it.principal as Profile
+ }.awaitSingle()
+ service.profile = profile
+ }
+ }
+
+ /**
* Binds a given route with a function of the receiver.
* @param function a function of the receiver
* @param method a HTTP method
@@ -80,35 +127,34 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
method: HttpMethod, route: String?
) {
val routeDef = route ?: "route${this::class.simpleName}${counter++}"
- addRoute(method, "/kv/$routeDef") { req, res, ctx ->
+ addRoute(method, "/kv/$routeDef") { req, ctx ->
val service = ctx.getBean(serviceClass.java)
+ initializeService(service, req)
val jsonRpcRequest = if (method == HttpMethod.GET) {
- JsonRpcRequest(req.getParameter("id")?.toInt() ?: 0, "", listOf())
+ JsonRpcRequest(req.queryParam("id").orElse(null)?.toInt() ?: 0, "", listOf())
} else {
- mapper.readValue(req.inputStream, JsonRpcRequest::class.java)
+ req.awaitBody()
}
- GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
- try {
- val result = function.invoke(service)
- res.writeJSON(
- mapper.writeValueAsString(
- JsonRpcResponse(
- id = jsonRpcRequest.id,
- result = mapper.writeValueAsString(result)
- )
+ try {
+ val result = function.invoke(service)
+ ServerResponse.ok().json().bodyValueAndAwait(
+ mapper.writeValueAsString(
+ JsonRpcResponse(
+ id = jsonRpcRequest.id,
+ result = mapper.writeValueAsString(result)
)
)
- } catch (e: Exception) {
- LOG.error(e.message, e)
- res.writeJSON(
- mapper.writeValueAsString(
- JsonRpcResponse(
- id = jsonRpcRequest.id,
- error = e.message ?: "Error"
- )
+ )
+ } catch (e: Exception) {
+ LOG.error(e.message, e)
+ ServerResponse.ok().json().bodyValueAndAwait(
+ mapper.writeValueAsString(
+ JsonRpcResponse(
+ id = jsonRpcRequest.id,
+ error = e.message ?: "Error"
)
)
- }
+ )
}
}
}
@@ -127,36 +173,35 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
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, res, ctx ->
+ addRoute(method, "/kv/$routeDef") { req, ctx ->
val service = ctx.getBean(serviceClass.java)
- val jsonRpcRequest = mapper.readValue(req.inputStream, JsonRpcRequest::class.java)
+ initializeService(service, req)
+ val jsonRpcRequest = req.awaitBody<JsonRpcRequest>()
if (jsonRpcRequest.params.size == 1) {
val param = getParameter<PAR>(jsonRpcRequest.params[0])
- GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
- try {
- val result = function.invoke(service, param)
- res.writeJSON(
- mapper.writeValueAsString(
- JsonRpcResponse(
- id = jsonRpcRequest.id,
- result = mapper.writeValueAsString(result)
- )
+ try {
+ val result = function.invoke(service, param)
+ ServerResponse.ok().json().bodyValueAndAwait(
+ mapper.writeValueAsString(
+ JsonRpcResponse(
+ id = jsonRpcRequest.id,
+ result = mapper.writeValueAsString(result)
)
)
- } catch (e: Exception) {
- LOG.error(e.message, e)
- res.writeJSON(
- mapper.writeValueAsString(
- JsonRpcResponse(
- id = jsonRpcRequest.id,
- error = e.message ?: "Error"
- )
+ )
+ } catch (e: Exception) {
+ LOG.error(e.message, e)
+ ServerResponse.ok().json().bodyValueAndAwait(
+ mapper.writeValueAsString(
+ JsonRpcResponse(
+ id = jsonRpcRequest.id,
+ error = e.message ?: "Error"
)
)
- }
+ )
}
} else {
- res.writeJSON(
+ ServerResponse.ok().json().bodyValueAndAwait(
mapper.writeValueAsString(
JsonRpcResponse(
id = jsonRpcRequest.id,
@@ -182,37 +227,36 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
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, res, ctx ->
+ addRoute(method, "/kv/$routeDef") { req, ctx ->
val service = ctx.getBean(serviceClass.java)
- val jsonRpcRequest = mapper.readValue(req.inputStream, JsonRpcRequest::class.java)
+ initializeService(service, req)
+ val jsonRpcRequest = req.awaitBody<JsonRpcRequest>()
if (jsonRpcRequest.params.size == 2) {
val param1 = getParameter<PAR1>(jsonRpcRequest.params[0])
val param2 = getParameter<PAR2>(jsonRpcRequest.params[1])
- GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
- try {
- val result = function.invoke(service, param1, param2)
- res.writeJSON(
- mapper.writeValueAsString(
- JsonRpcResponse(
- id = jsonRpcRequest.id,
- result = mapper.writeValueAsString(result)
- )
+ 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) {
- LOG.error(e.message, e)
- res.writeJSON(
- mapper.writeValueAsString(
- JsonRpcResponse(
- id = jsonRpcRequest.id,
- error = e.message ?: "Error"
- )
+ )
+ } catch (e: Exception) {
+ LOG.error(e.message, e)
+ ServerResponse.ok().json().bodyValueAndAwait(
+ mapper.writeValueAsString(
+ JsonRpcResponse(
+ id = jsonRpcRequest.id,
+ error = e.message ?: "Error"
)
)
- }
+ )
}
} else {
- res.writeJSON(
+ ServerResponse.ok().json().bodyValueAndAwait(
mapper.writeValueAsString(
JsonRpcResponse(
id = jsonRpcRequest.id,
@@ -238,39 +282,38 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
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, res, ctx ->
+ addRoute(method, "/kv/$routeDef") { req, ctx ->
val service = ctx.getBean(serviceClass.java)
- val jsonRpcRequest = mapper.readValue(req.inputStream, JsonRpcRequest::class.java)
+ initializeService(service, req)
+ val jsonRpcRequest = req.awaitBody<JsonRpcRequest>()
@Suppress("MagicNumber")
if (jsonRpcRequest.params.size == 3) {
val param1 = getParameter<PAR1>(jsonRpcRequest.params[0])
val param2 = getParameter<PAR2>(jsonRpcRequest.params[1])
val param3 = getParameter<PAR3>(jsonRpcRequest.params[2])
- GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
- try {
- val result = function.invoke(service, param1, param2, param3)
- res.writeJSON(
- mapper.writeValueAsString(
- JsonRpcResponse(
- id = jsonRpcRequest.id,
- result = mapper.writeValueAsString(result)
- )
+ 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) {
- LOG.error(e.message, e)
- res.writeJSON(
- mapper.writeValueAsString(
- JsonRpcResponse(
- id = jsonRpcRequest.id,
- error = e.message ?: "Error"
- )
+ )
+ } catch (e: Exception) {
+ LOG.error(e.message, e)
+ ServerResponse.ok().json().bodyValueAndAwait(
+ mapper.writeValueAsString(
+ JsonRpcResponse(
+ id = jsonRpcRequest.id,
+ error = e.message ?: "Error"
)
)
- }
+ )
}
} else {
- res.writeJSON(
+ ServerResponse.ok().json().bodyValueAndAwait(
mapper.writeValueAsString(
JsonRpcResponse(
id = jsonRpcRequest.id,
@@ -296,40 +339,39 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
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, res, ctx ->
+ addRoute(method, "/kv/$routeDef") { req, ctx ->
val service = ctx.getBean(serviceClass.java)
- val jsonRpcRequest = mapper.readValue(req.inputStream, JsonRpcRequest::class.java)
+ initializeService(service, req)
+ val jsonRpcRequest = req.awaitBody<JsonRpcRequest>()
@Suppress("MagicNumber")
if (jsonRpcRequest.params.size == 4) {
val param1 = getParameter<PAR1>(jsonRpcRequest.params[0])
val param2 = getParameter<PAR2>(jsonRpcRequest.params[1])
val param3 = getParameter<PAR3>(jsonRpcRequest.params[2])
val param4 = getParameter<PAR4>(jsonRpcRequest.params[3])
- GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
- try {
- val result = function.invoke(service, param1, param2, param3, param4)
- res.writeJSON(
- mapper.writeValueAsString(
- JsonRpcResponse(
- id = jsonRpcRequest.id,
- result = mapper.writeValueAsString(result)
- )
+ 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) {
- LOG.error(e.message, e)
- res.writeJSON(
- mapper.writeValueAsString(
- JsonRpcResponse(
- id = jsonRpcRequest.id,
- error = e.message ?: "Error"
- )
+ )
+ } catch (e: Exception) {
+ LOG.error(e.message, e)
+ ServerResponse.ok().json().bodyValueAndAwait(
+ mapper.writeValueAsString(
+ JsonRpcResponse(
+ id = jsonRpcRequest.id,
+ error = e.message ?: "Error"
)
)
- }
+ )
}
} else {
- res.writeJSON(
+ ServerResponse.ok().json().bodyValueAndAwait(
mapper.writeValueAsString(
JsonRpcResponse(
id = jsonRpcRequest.id,
@@ -356,9 +398,10 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
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, res, ctx ->
+ addRoute(method, "/kv/$routeDef") { req, ctx ->
val service = ctx.getBean(serviceClass.java)
- val jsonRpcRequest = mapper.readValue(req.inputStream, JsonRpcRequest::class.java)
+ initializeService(service, req)
+ val jsonRpcRequest = req.awaitBody<JsonRpcRequest>()
@Suppress("MagicNumber")
if (jsonRpcRequest.params.size == 5) {
val param1 = getParameter<PAR1>(jsonRpcRequest.params[0])
@@ -366,31 +409,29 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
val param3 = getParameter<PAR3>(jsonRpcRequest.params[2])
val param4 = getParameter<PAR4>(jsonRpcRequest.params[3])
val param5 = getParameter<PAR5>(jsonRpcRequest.params[4])
- GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
- try {
- val result = function.invoke(service, param1, param2, param3, param4, param5)
- res.writeJSON(
- mapper.writeValueAsString(
- JsonRpcResponse(
- id = jsonRpcRequest.id,
- result = mapper.writeValueAsString(result)
- )
+ 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) {
- LOG.error(e.message, e)
- res.writeJSON(
- mapper.writeValueAsString(
- JsonRpcResponse(
- id = jsonRpcRequest.id,
- error = e.message ?: "Error"
- )
+ )
+ } catch (e: Exception) {
+ LOG.error(e.message, e)
+ ServerResponse.ok().json().bodyValueAndAwait(
+ mapper.writeValueAsString(
+ JsonRpcResponse(
+ id = jsonRpcRequest.id,
+ error = e.message ?: "Error"
)
)
- }
+ )
}
} else {
- res.writeJSON(
+ ServerResponse.ok().json().bodyValueAndAwait(
mapper.writeValueAsString(
JsonRpcResponse(
id = jsonRpcRequest.id,
@@ -413,9 +454,19 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
) {
val routeDef = "route${this::class.simpleName}${counter++}"
webSocketsRequests[routeDef] = { webSocketSession, ctx, incoming, outgoing ->
- val service = synchronized(this) {
- WebSocketSessionHolder.webSocketSession = webSocketSession
- ctx.getBean(serviceClass.java)
+ 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
+ }
+ if (service is WithProfile) {
+ val profile = webSocketSession.handshakeInfo.principal.ofType(Authentication::class.java).map {
+ it.principal as Profile
+ }.awaitSingle()
+ service.profile = profile
}
val requestChannel = Channel<PAR1>()
val responseChannel = Channel<PAR2>()
@@ -440,6 +491,7 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
)
outgoing.send(text)
}
+ if (!incoming.isClosedForReceive) incoming.cancel()
}
launch(start = CoroutineStart.UNDISPATCHED) {
function.invoke(service, requestChannel, responseChannel)
@@ -458,15 +510,16 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
function: T.(String?, String?) -> List<RemoteOption>
) {
val routeDef = "route${this::class.simpleName}${counter++}"
- addRoute(HttpMethod.POST, "/kv/$routeDef") { req, res, ctx ->
+ addRoute(HttpMethod.POST, "/kv/$routeDef") { req, ctx ->
val service = ctx.getBean(serviceClass.java)
- val jsonRpcRequest = mapper.readValue(req.inputStream, JsonRpcRequest::class.java)
+ initializeService(service, req)
+ val jsonRpcRequest = req.awaitBody<JsonRpcRequest>()
if (jsonRpcRequest.params.size == 2) {
val param1 = getParameter<String?>(jsonRpcRequest.params[0])
val param2 = getParameter<String?>(jsonRpcRequest.params[1])
try {
val result = function.invoke(service, param1, param2)
- res.writeJSON(
+ ServerResponse.ok().json().bodyValueAndAwait(
mapper.writeValueAsString(
JsonRpcResponse(
id = jsonRpcRequest.id,
@@ -476,7 +529,7 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
)
} catch (e: Exception) {
LOG.error(e.message, e)
- res.writeJSON(
+ ServerResponse.ok().json().bodyValueAndAwait(
mapper.writeValueAsString(
JsonRpcResponse(
id = jsonRpcRequest.id,
@@ -486,7 +539,7 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
)
}
} else {
- res.writeJSON(
+ ServerResponse.ok().json().bodyValueAndAwait(
mapper.writeValueAsString(
JsonRpcResponse(
id = jsonRpcRequest.id,
@@ -507,9 +560,10 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
noinline function: T.(Int?, Int?, List<RemoteFilter>?, List<RemoteSorter>?) -> RemoteData<RET>
) {
val routeDef = "route${this::class.simpleName}${counter++}"
- addRoute(HttpMethod.POST, "/kv/$routeDef") { req, res, ctx ->
+ addRoute(HttpMethod.POST, "/kv/$routeDef") { req, ctx ->
val service = ctx.getBean(serviceClass.java)
- val jsonRpcRequest = mapper.readValue(req.inputStream, JsonRpcRequest::class.java)
+ initializeService(service, req)
+ val jsonRpcRequest = req.awaitBody<JsonRpcRequest>()
@Suppress("MagicNumber")
if (jsonRpcRequest.params.size == 4) {
val param1 = getParameter<Int?>(jsonRpcRequest.params[0])
@@ -519,7 +573,7 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
val param4 = getParameter<List<RemoteSorter>?>(jsonRpcRequest.params[3])
try {
val result = function.invoke(service, param1, param2, param3, param4)
- res.writeJSON(
+ ServerResponse.ok().json().bodyValueAndAwait(
mapper.writeValueAsString(
JsonRpcResponse(
id = jsonRpcRequest.id,
@@ -529,7 +583,7 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
)
} catch (e: Exception) {
LOG.error(e.message, e)
- res.writeJSON(
+ ServerResponse.ok().json().bodyValueAndAwait(
mapper.writeValueAsString(
JsonRpcResponse(
id = jsonRpcRequest.id,
@@ -539,7 +593,7 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
)
}
} else {
- res.writeJSON(
+ ServerResponse.ok().json().bodyValueAndAwait(
mapper.writeValueAsString(
JsonRpcResponse(
id = jsonRpcRequest.id,
@@ -557,7 +611,7 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
fun addRoute(
method: HttpMethod,
path: String,
- handler: (HttpServletRequest, HttpServletResponse, ApplicationContext) -> Unit
+ handler: suspend (ServerRequest, ApplicationContext) -> ServerResponse
) {
when (method) {
HttpMethod.GET -> getRequests[path] = handler
@@ -581,14 +635,3 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass:
} ?: null as T
}
}
-
-/**
- * @suppress internal function
- */
-fun HttpServletResponse.writeJSON(json: String) {
- val out = this.outputStream
- this.contentType = "application/json"
- this.characterEncoding = "UTF-8"
- out.write(json.toByteArray())
- out.flush()
-}
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
index 17c897ef..482c2526 100644
--- 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
@@ -21,232 +21,87 @@
*/
package pl.treksoft.kvision.remote
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.ObsoleteCoroutinesApi
+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.launch
-import org.springframework.beans.factory.annotation.Autowired
-import org.springframework.beans.factory.config.ConfigurableBeanFactory
+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.context.annotation.Scope
-import org.springframework.http.HttpHeaders
-import org.springframework.http.server.ServerHttpRequest
-import org.springframework.http.server.ServerHttpResponse
-import org.springframework.web.context.support.GenericWebApplicationContext
-import org.springframework.web.socket.CloseStatus
-import org.springframework.web.socket.TextMessage
-import org.springframework.web.socket.WebSocketExtension
-import org.springframework.web.socket.WebSocketHandler
-import org.springframework.web.socket.WebSocketMessage
-import org.springframework.web.socket.WebSocketSession
-import org.springframework.web.socket.config.annotation.EnableWebSocket
-import org.springframework.web.socket.config.annotation.WebSocketConfigurer
-import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
-import org.springframework.web.socket.handler.TextWebSocketHandler
-import org.springframework.web.socket.server.HandshakeInterceptor
-import java.net.InetSocketAddress
-import java.net.URI
-import java.security.Principal
-import java.util.concurrent.ConcurrentHashMap
-
-const val KV_ROUTE_ID_ATTRIBUTE = "KV_ROUTE_ID_ATTRIBUTE"
-
-/**
- * Automatic websockets configuration.
- */
-@Configuration
-@EnableWebSocket
-open class KVWebSocketConfig : WebSocketConfigurer {
-
- @Autowired
- lateinit var services: List<KVServiceManager<*>>
-
- @Autowired
- lateinit var applicationContext: GenericWebApplicationContext
-
- override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
- registry.addHandler(socketHandler(), "/kvws/*").setAllowedOrigins("*").addInterceptors(routeInterceptor())
- }
-
- @Bean
- open fun routeInterceptor(): HandshakeInterceptor {
- return KvHandshakeInterceptor()
- }
-
- @Bean
- open fun socketHandler(): WebSocketHandler {
- return KvWebSocketHandler(services, applicationContext)
- }
-
- @Bean
- @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
- open fun webSocketSession(): WebSocketSession {
- return WebSocketSessionHolder.webSocketSession
- }
-}
-
-object WebSocketSessionHolder {
- var webSocketSession: WebSocketSession = DummyWebSocketSession()
-}
-
-internal open class KvHandshakeInterceptor : HandshakeInterceptor {
- override fun beforeHandshake(
- request: ServerHttpRequest,
- response: ServerHttpResponse,
- wsHandler: WebSocketHandler,
- attributes: MutableMap<String, Any>
- ): Boolean {
- val path = request.uri.path
- val route = path.substring(path.lastIndexOf('/') + 1)
- attributes[KV_ROUTE_ID_ATTRIBUTE] = route
- return true
- }
-
- override fun afterHandshake(
- request: ServerHttpRequest,
- response: ServerHttpResponse,
- wsHandler: WebSocketHandler,
- exception: Exception?
- ) {
- }
-}
-
-@UseExperimental(ExperimentalCoroutinesApi::class)
-internal open class KvWebSocketHandler(
+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<KVServiceManager<*>>,
- private val applicationContext: GenericWebApplicationContext
-) : TextWebSocketHandler() {
-
- private val sessions = ConcurrentHashMap<String, Pair<Channel<String>, Channel<String>>>()
+ private val applicationContext: ApplicationContext
+) : WebSocketHandler, CoroutineScope by CoroutineScope(Dispatchers.Default) {
private fun getHandler(session: WebSocketSession): (suspend (
- WebSocketSession, GenericWebApplicationContext,
+ WebSocketSession, ApplicationContext,
ReceiveChannel<String>, SendChannel<String>
- ) -> Unit)? {
- val routeId = session.attributes[KV_ROUTE_ID_ATTRIBUTE] as String
+ ) -> Unit) {
+ val uri = session.handshakeInfo.uri.toString()
+ val route = uri.substring(uri.lastIndexOf('/') + 1)
return services.mapNotNull {
- it.webSocketsRequests[routeId]
- }.firstOrNull()
- }
-
- private fun getSessionId(session: WebSocketSession): String {
- val routeId = session.attributes[KV_ROUTE_ID_ATTRIBUTE] as String
- return session.id + "###" + routeId
- }
-
- override fun afterConnectionEstablished(session: WebSocketSession) {
- getHandler(session)?.let { handler ->
- val requestChannel = Channel<String>()
- val responseChannel = Channel<String>()
- GlobalScope.launch {
- coroutineScope {
- launch(Dispatchers.IO) {
- for (text in responseChannel) {
- session.sendMessage(TextMessage(text))
- }
- session.close()
- }
- launch {
- handler.invoke(session, applicationContext, requestChannel, responseChannel)
- if (!responseChannel.isClosedForReceive) responseChannel.close()
+ it.webSocketsRequests[route]
+ }.first()
+ }
+
+ @UseExperimental(ObsoleteCoroutinesApi::class, ExperimentalCoroutinesApi::class)
+ override fun handle(session: WebSocketSession): Mono<Void> {
+ val handler = getHandler(session)
+ val responseChannel = Channel<String>()
+ val requestChannel = Channel<String>()
+ val output = session.send(responseChannel.asFlux(EmptyCoroutineContext).map(session::textMessage))
+ val input = async {
+ coroutineScope {
+ launch {
+ session.receive().map {
+ it.payloadAsText
+ }.asFlow().collect {
+ requestChannel.send(it)
}
- sessions[getSessionId(session)] = responseChannel to requestChannel
- }
- }
- }
- }
-
- override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
- getHandler(session)?.let {
- sessions[getSessionId(session)]?.let { (_, requestChannel) ->
- GlobalScope.launch {
- requestChannel.send(message.payload)
- }
- }
- }
- }
-
- override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
- getHandler(session)?.let {
- sessions[getSessionId(session)]?.let { (responseChannel, requestChannel) ->
- GlobalScope.launch {
- responseChannel.close()
requestChannel.close()
}
- sessions.remove(getSessionId(session))
+ launch {
+ handler.invoke(session, applicationContext, requestChannel, responseChannel)
+ if (!responseChannel.isClosedForReceive) responseChannel.close()
+ session.close()
+ }
}
- }
+ }.asMono(EmptyCoroutineContext).then()
+ return Mono.zip(input, output).then()
}
}
-@Suppress("TooManyFunctions")
-open class DummyWebSocketSession : WebSocketSession {
- override fun getBinaryMessageSizeLimit(): Int {
- return 0
- }
-
- override fun sendMessage(message: WebSocketMessage<*>) {
- }
-
- override fun getAcceptedProtocol(): String? {
- return null
- }
-
- override fun getTextMessageSizeLimit(): Int {
- return 0
- }
-
- override fun getLocalAddress(): InetSocketAddress? {
- return null
- }
-
- override fun getId(): String {
- return ""
- }
-
- override fun getExtensions(): MutableList<WebSocketExtension> {
- return mutableListOf()
- }
-
- override fun getUri(): URI? {
- return null
- }
-
- override fun setBinaryMessageSizeLimit(messageSizeLimit: Int) {
- }
-
- override fun getAttributes(): MutableMap<String, Any> {
- return mutableMapOf()
- }
-
- override fun getHandshakeHeaders(): HttpHeaders {
- return HttpHeaders.EMPTY
- }
-
- override fun isOpen(): Boolean {
- return false
- }
-
- override fun getPrincipal(): Principal? {
- return null
- }
-
- override fun close() {
- }
-
- override fun close(status: CloseStatus) {
- }
-
- override fun setTextMessageSizeLimit(messageSizeLimit: Int) {
- }
+@Configuration
+open class KVWebSocketConfig(
+ private var services: List<KVServiceManager<*>>,
+ private var applicationContext: ApplicationContext
+) {
- override fun getRemoteAddress(): InetSocketAddress? {
- return null
+ @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/Profile.kt b/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/Profile.kt
index 18164800..2831b220 100644
--- a/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/Profile.kt
+++ b/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/Profile.kt
@@ -21,32 +21,123 @@
*/
package pl.treksoft.kvision.remote
-import org.pac4j.core.context.J2EContext
-import org.pac4j.core.context.session.J2ESessionStore
-import org.pac4j.core.profile.CommonProfile
-import org.pac4j.core.profile.ProfileManager
-import org.springframework.web.context.request.RequestContextHolder
-import org.springframework.web.context.request.ServletRequestAttributes
+import com.fasterxml.jackson.annotation.JsonIgnore
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import org.springframework.security.core.GrantedAuthority
+import org.springframework.security.core.userdetails.UserDetails
/**
* A user profile.
*/
-actual typealias Profile = CommonProfile
+@Serializable
+actual data class Profile(
+ val id: String? = null,
+ val attributes: MutableMap<String, String> = mutableMapOf(),
+ val authenticationAttributes: MutableMap<String, String> = mutableMapOf(),
+ val roles: MutableSet<String> = mutableSetOf(),
+ val permissions: MutableSet<String> = mutableSetOf(),
+ val linkedId: String? = null,
+ val remembered: Boolean = false,
+ val clientName: String? = null
+) : UserDetails {
-/**
- * A helper extension function for processing with authenticated user profile.
- */
-@Suppress("TooGenericExceptionCaught")
-suspend fun <RESP> withProfile(block: suspend (Profile) -> RESP): RESP {
- val profile = try {
- val requestAttributes = (RequestContextHolder.getRequestAttributes() as ServletRequestAttributes)
- val req = requestAttributes.request
- val resp = requestAttributes.response
- ProfileManager<CommonProfile>(J2EContext(req, resp, J2ESessionStore())).get(true).get()
- } catch (e: Exception) {
- null
- }
- return profile?.let {
- block(it)
- } ?: throw IllegalStateException("Profile not set!")
+ @Transient
+ @JsonIgnore
+ private var password: String? = null
+
+ override fun getUsername(): String? {
+ return attributes["username"]
+ }
+
+ fun setUsername(username: String?) {
+ if (username != null) {
+ attributes["username"] = username
+ } else {
+ attributes.remove("username")
+ }
+ }
+
+ override fun getPassword(): String? {
+ return password
+ }
+
+ fun setPassword(password: String?) {
+ this.password = password
+ }
+
+ override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
+ return mutableListOf()
+ }
+
+ override fun isEnabled(): Boolean {
+ return true
+ }
+
+ override fun isCredentialsNonExpired(): Boolean {
+ return true
+ }
+
+ override fun isAccountNonExpired(): Boolean {
+ return true
+ }
+
+ override fun isAccountNonLocked(): Boolean {
+ return true
+ }
+
+ var firstName: String?
+ get() = attributes["first_name"]
+ set(value) {
+ if (value != null) {
+ attributes["first_name"] = value
+ } else {
+ attributes.remove("first_name")
+ }
+ }
+ var familyName: String?
+ get() = attributes["family_name"]
+ set(value) {
+ if (value != null) {
+ attributes["family_name"] = value
+ } else {
+ attributes.remove("family_name")
+ }
+ }
+ var displayName: String?
+ get() = attributes["display_name"]
+ set(value) {
+ if (value != null) {
+ attributes["display_name"] = value
+ } else {
+ attributes.remove("display_name")
+ }
+ }
+ var email: String?
+ get() = attributes["email"]
+ set(value) {
+ if (value != null) {
+ attributes["email"] = value
+ } else {
+ attributes.remove("email")
+ }
+ }
+ var pictureUrl: String?
+ get() = attributes["picture_url"]
+ set(value) {
+ if (value != null) {
+ attributes["picture_url"] = value
+ } else {
+ attributes.remove("picture_url")
+ }
+ }
+ var profileUrl: String?
+ get() = attributes["profile_url"]
+ set(value) {
+ if (value != null) {
+ attributes["profile_url"] = value
+ } else {
+ attributes.remove("profile_url")
+ }
+ }
}
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
index 287a3a7e..e87b95ff 100644
--- 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
@@ -21,14 +21,30 @@
*/
package pl.treksoft.kvision.remote
-import org.springframework.web.servlet.config.annotation.InterceptorRegistration
+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 an interceptor from a list of service managers.
+ * A function to gather paths for spring security matchers.
*/
-fun InterceptorRegistration.addPathPatternsFromServices(services: List<KVServiceManager<*>>) {
- val paths = services.flatMap {
- it.postRequests.keys + it.putRequests.keys + it.optionsRequests.keys + it.optionsRequests.keys
- }
- this.addPathPatterns(paths)
+fun ServerHttpSecurity.AuthorizeExchangeSpec.serviceMatchers(vararg services: KVServiceManager<*>): ServerHttpSecurity.AuthorizeExchangeSpec.Access {
+ val matchers = mutableListOf<ServerWebExchangeMatcher>()
+ 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 this.matchers(*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
new file mode 100644
index 00000000..d35cc97a
--- /dev/null
+++ b/kvision-modules/kvision-server-spring-boot/src/main/kotlin/pl/treksoft/kvision/remote/SessionInterfaces.kt
@@ -0,0 +1,47 @@
+/*
+ * 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 WithProfile {
+ var profile: Profile
+}
+
+interface WithWebSocketSession {
+ var webSocketSession: WebSocketSession
+}
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
index 17ca7a1d..01084666 100644
--- 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
@@ -1 +1 @@
-org.springframework.boot.autoconfigure.EnableAutoConfiguration=pl.treksoft.kvision.remote.KVController,pl.treksoft.kvision.remote.KVWebSocketConfig
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=pl.treksoft.kvision.remote.KVRouterConfiguration,pl.treksoft.kvision.remote.KVHandler,pl.treksoft.kvision.remote.KVWebSocketConfig