diff options
16 files changed, 668 insertions, 480 deletions
diff --git a/gradle.properties b/gradle.properties index 59d36c5e..3ac77b8c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=pl.treksoft -version=2.0.0-M1 +version=2.0.0-SNAPSHOT kotlinVersion=1.3.50 javaVersion=1.8 coroutinesVersion=1.3.1 @@ -9,12 +9,13 @@ dokkaVersion=0.9.18 detektVersion=1.0.0-RC14 junitVersion=4.12 joobyVersion=1.6.4 -springBootVersion=2.1.8.RELEASE +springBootVersion=2.2.0.RC1 ktorVersion=1.2.4 guiceVersion=4.2.2 pac4jVersion=3.5.0 dependencyManagementPluginVersion=1.0.4.RELEASE jacksonModuleKotlinVersion=2.9.10 +springDataRelationalVersion=1.1.0.RELEASE jqueryKotlinVersion=0.0.4 snabbdomKotlinVersion=0.1.1 navigoKotlinVersion=0.0.3 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 |