diff options
| author | Robert Jaros <rjaros@finn.pl> | 2019-10-12 18:22:43 +0200 |
|---|---|---|
| committer | Robert Jaros <rjaros@finn.pl> | 2019-10-12 18:22:43 +0200 |
| commit | bcf7504392baccf1568e740c1d453eac7080fb5b (patch) | |
| tree | 519489751032814bfc0d5426c48de257b788329f | |
| parent | 28b6e9d646123d774660ebdf0124eeb8a22fe087 (diff) | |
| download | kvision-bcf7504392baccf1568e740c1d453eac7080fb5b.tar.gz kvision-bcf7504392baccf1568e740c1d453eac7080fb5b.tar.bz2 kvision-bcf7504392baccf1568e740c1d453eac7080fb5b.zip | |
Redesign spring-boot server module to use Spring WebFlux instead of Spring MVC.
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, |
