From 702c6965a9f7856f59257b55a1927fbc834d26e0 Mon Sep 17 00:00:00 2001 From: Robert Jaros Date: Thu, 16 Apr 2020 17:31:38 +0200 Subject: Deprecate session interfaces in favour of dependency injection --- .../kotlin/pl/treksoft/kvision/remote/Helpers.kt | 193 +++++++++++++++++++++ .../kvision/remote/KVRouterConfiguration.kt | 21 ++- .../pl/treksoft/kvision/remote/KVServiceManager.kt | 53 ++++-- .../treksoft/kvision/remote/KVWebSocketConfig.kt | 32 +++- .../treksoft/kvision/remote/SessionInterfaces.kt | 4 + 5 files changed, 279 insertions(+), 24 deletions(-) create mode 100644 kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/Helpers.kt (limited to 'kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision') diff --git a/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/Helpers.kt b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/Helpers.kt new file mode 100644 index 00000000..fb128a90 --- /dev/null +++ b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/Helpers.kt @@ -0,0 +1,193 @@ +/* + * 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.reactivestreams.Publisher +import org.springframework.core.ParameterizedTypeReference +import org.springframework.core.io.buffer.DataBuffer +import org.springframework.core.io.buffer.DataBufferFactory +import org.springframework.http.HttpCookie +import org.springframework.http.codec.HttpMessageReader +import org.springframework.http.codec.multipart.Part +import org.springframework.http.server.reactive.ServerHttpRequest +import org.springframework.util.MultiValueMap +import org.springframework.web.reactive.function.BodyExtractor +import org.springframework.web.reactive.function.server.ServerRequest +import org.springframework.web.reactive.socket.CloseStatus +import org.springframework.web.reactive.socket.HandshakeInfo +import org.springframework.web.reactive.socket.WebSocketMessage +import org.springframework.web.reactive.socket.WebSocketSession +import org.springframework.web.server.ServerWebExchange +import org.springframework.web.server.WebSession +import org.springframework.web.util.UriBuilder +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.net.InetSocketAddress +import java.net.URI +import java.security.Principal +import java.util.* +import java.util.function.Function + +/** + * Empty implementation of the ServerRequest interface + */ +internal class KVServerRequest : ServerRequest { + override fun body(extractor: BodyExtractor): T { + throw IllegalStateException("Empty implementation") + } + + override fun body(extractor: BodyExtractor, hints: MutableMap): T { + throw IllegalStateException("Empty implementation") + } + + override fun remoteAddress(): Optional { + throw IllegalStateException("Empty implementation") + } + + override fun attributes(): MutableMap { + throw IllegalStateException("Empty implementation") + } + + override fun session(): Mono { + throw IllegalStateException("Empty implementation") + } + + override fun principal(): Mono { + throw IllegalStateException("Empty implementation") + } + + override fun multipartData(): Mono> { + throw IllegalStateException("Empty implementation") + } + + override fun messageReaders(): MutableList> { + throw IllegalStateException("Empty implementation") + } + + override fun uri(): URI { + throw IllegalStateException("Empty implementation") + } + + override fun exchange(): ServerWebExchange { + throw IllegalStateException("Empty implementation") + } + + override fun bodyToFlux(elementClass: Class): Flux { + throw IllegalStateException("Empty implementation") + } + + override fun bodyToFlux(typeReference: ParameterizedTypeReference): Flux { + throw IllegalStateException("Empty implementation") + } + + override fun formData(): Mono> { + throw IllegalStateException("Empty implementation") + } + + override fun queryParams(): MultiValueMap { + throw IllegalStateException("Empty implementation") + } + + override fun pathVariables(): MutableMap { + throw IllegalStateException("Empty implementation") + } + + override fun localAddress(): Optional { + throw IllegalStateException("Empty implementation") + } + + override fun uriBuilder(): UriBuilder { + throw IllegalStateException("Empty implementation") + } + + override fun methodName(): String { + throw IllegalStateException("Empty implementation") + } + + override fun bodyToMono(elementClass: Class): Mono { + throw IllegalStateException("Empty implementation") + } + + override fun bodyToMono(typeReference: ParameterizedTypeReference): Mono { + throw IllegalStateException("Empty implementation") + } + + override fun cookies(): MultiValueMap { + throw IllegalStateException("Empty implementation") + } + + override fun headers(): ServerRequest.Headers { + throw IllegalStateException("Empty implementation") + } + +} + +/** + * Empty implementation of the WebSocketSession interface + */ +internal class KVWebSocketSession : WebSocketSession { + override fun getId(): String { + throw IllegalStateException("Empty implementation") + } + + override fun binaryMessage(payloadFactory: Function): WebSocketMessage { + throw IllegalStateException("Empty implementation") + } + + override fun pingMessage(payloadFactory: Function): WebSocketMessage { + throw IllegalStateException("Empty implementation") + } + + override fun bufferFactory(): DataBufferFactory { + throw IllegalStateException("Empty implementation") + } + + override fun getAttributes(): MutableMap { + throw IllegalStateException("Empty implementation") + } + + override fun receive(): Flux { + throw IllegalStateException("Empty implementation") + } + + override fun pongMessage(payloadFactory: Function): WebSocketMessage { + throw IllegalStateException("Empty implementation") + } + + override fun getHandshakeInfo(): HandshakeInfo { + throw IllegalStateException("Empty implementation") + } + + override fun send(messages: Publisher): Mono { + throw IllegalStateException("Empty implementation") + } + + override fun close(status: CloseStatus): Mono { + throw IllegalStateException("Empty implementation") + } + + override fun textMessage(payload: String): WebSocketMessage { + throw IllegalStateException("Empty implementation") + } + +} diff --git a/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVRouterConfiguration.kt b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVRouterConfiguration.kt index 42c4107d..80ec896b 100644 --- a/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVRouterConfiguration.kt +++ b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVRouterConfiguration.kt @@ -22,10 +22,11 @@ package pl.treksoft.kvision.remote import org.springframework.beans.factory.annotation.Value +import org.springframework.beans.factory.config.BeanDefinition 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.context.annotation.Scope import org.springframework.core.io.Resource import org.springframework.http.MediaType.TEXT_HTML import org.springframework.stereotype.Component @@ -35,6 +36,9 @@ import org.springframework.web.reactive.function.server.buildAndAwait import org.springframework.web.reactive.function.server.coRouter import org.springframework.web.reactive.function.server.router +/** + * Default Spring Boot routes + */ @Configuration open class KVRouterConfiguration { @Value("classpath:/public/index.html") @@ -57,8 +61,19 @@ open class KVRouterConfiguration { } } +/** + * Default Spring Boot handler + */ @Component -open class KVHandler(var services: List>, var applicationContext: ApplicationContext) { +open class KVHandler(val services: List>, val applicationContext: ApplicationContext) { + + private val threadLocalRequest = ThreadLocal() + + @Bean + @Scope(BeanDefinition.SCOPE_PROTOTYPE) + open fun serverRequest(): ServerRequest { + return threadLocalRequest.get() ?: KVServerRequest() + } open suspend fun handle(request: ServerRequest): ServerResponse { val routeUrl = request.path() @@ -73,7 +88,7 @@ open class KVHandler(var services: List>, var applicationCon } }.firstOrNull() return if (handler != null) { - handler(request, applicationContext as GenericApplicationContext) + handler(request, threadLocalRequest, applicationContext) } else { ServerResponse.notFound().buildAndAwait() } diff --git a/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt index 1e9bb949..eb602aa1 100644 --- a/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt +++ b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVServiceManager.kt @@ -59,18 +59,18 @@ actual open class KVServiceManager actual constructor(val serviceClass: val LOG: Logger = LoggerFactory.getLogger(KVServiceManager::class.java.name) } - val getRequests: MutableMap ServerResponse> = + val getRequests: MutableMap, ApplicationContext) -> ServerResponse> = mutableMapOf() - val postRequests: MutableMap ServerResponse> = + val postRequests: MutableMap, ApplicationContext) -> ServerResponse> = mutableMapOf() - val putRequests: MutableMap ServerResponse> = + val putRequests: MutableMap, ApplicationContext) -> ServerResponse> = mutableMapOf() - val deleteRequests: MutableMap ServerResponse> = + val deleteRequests: MutableMap, ApplicationContext) -> ServerResponse> = mutableMapOf() - val optionsRequests: MutableMap ServerResponse> = + val optionsRequests: MutableMap, ApplicationContext) -> ServerResponse> = mutableMapOf() val webSocketsRequests: MutableMap, SendChannel + WebSocketSession, ThreadLocal, ApplicationContext, ReceiveChannel, SendChannel ) -> Unit> = mutableMapOf() @@ -95,6 +95,7 @@ actual open class KVServiceManager actual constructor(val serviceClass: /** * @suppress internal function */ + @Suppress("DEPRECATION") suspend fun initializeService(service: T, req: ServerRequest) { if (service is WithRequest) { service.serverRequest = req @@ -121,8 +122,10 @@ actual open class KVServiceManager actual constructor(val serviceClass: method: HttpMethod, route: String? ) { val routeDef = route ?: "route${this::class.simpleName}${counter++}" - addRoute(method, "/kv/$routeDef") { req, ctx -> + addRoute(method, "/kv/$routeDef") { req, tlReq, ctx -> + tlReq.set(req) val service = ctx.getBean(serviceClass.java) + tlReq.remove() initializeService(service, req) val jsonRpcRequest = if (method == HttpMethod.GET) { JsonRpcRequest(req.queryParam("id").orElse(null)?.toInt() ?: 0, "", listOf()) @@ -168,8 +171,10 @@ actual open class KVServiceManager 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, ctx -> + addRoute(method, "/kv/$routeDef") { req, tlReq, ctx -> + tlReq.set(req) val service = ctx.getBean(serviceClass.java) + tlReq.remove() initializeService(service, req) val jsonRpcRequest = req.awaitBody() if (jsonRpcRequest.params.size == 1) { @@ -223,8 +228,10 @@ actual open class KVServiceManager 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, ctx -> + addRoute(method, "/kv/$routeDef") { req, tlReq, ctx -> + tlReq.set(req) val service = ctx.getBean(serviceClass.java) + tlReq.remove() initializeService(service, req) val jsonRpcRequest = req.awaitBody() if (jsonRpcRequest.params.size == 2) { @@ -279,8 +286,10 @@ actual open class KVServiceManager 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, ctx -> + addRoute(method, "/kv/$routeDef") { req, tlReq, ctx -> + tlReq.set(req) val service = ctx.getBean(serviceClass.java) + tlReq.remove() initializeService(service, req) val jsonRpcRequest = req.awaitBody() @Suppress("MagicNumber") @@ -337,8 +346,10 @@ actual open class KVServiceManager 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, ctx -> + addRoute(method, "/kv/$routeDef") { req, tlReq, ctx -> + tlReq.set(req) val service = ctx.getBean(serviceClass.java) + tlReq.remove() initializeService(service, req) val jsonRpcRequest = req.awaitBody() @Suppress("MagicNumber") @@ -397,8 +408,10 @@ actual open class KVServiceManager 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, ctx -> + addRoute(method, "/kv/$routeDef") { req, tlReq, ctx -> + tlReq.set(req) val service = ctx.getBean(serviceClass.java) + tlReq.remove() initializeService(service, req) val jsonRpcRequest = req.awaitBody() @Suppress("MagicNumber") @@ -458,8 +471,10 @@ actual open class KVServiceManager 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, ctx -> + addRoute(method, "/kv/$routeDef") { req, tlReq, ctx -> + tlReq.set(req) val service = ctx.getBean(serviceClass.java) + tlReq.remove() initializeService(service, req) val jsonRpcRequest = req.awaitBody() @Suppress("MagicNumber") @@ -516,11 +531,15 @@ actual open class KVServiceManager actual constructor(val serviceClass: route: String? ) { val routeDef = "route${this::class.simpleName}${counter++}" - webSocketsRequests[routeDef] = { webSocketSession, ctx, incoming, outgoing -> + webSocketsRequests[routeDef] = { webSocketSession, tlWsSession, ctx, incoming, outgoing -> + tlWsSession.set(webSocketSession) val service = ctx.getBean(serviceClass.java) + tlWsSession.remove() + @Suppress("DEPRECATION") if (service is WithWebSocketSession) { service.webSocketSession = webSocketSession } + @Suppress("DEPRECATION") if (service is WithPrincipal) { val principal = webSocketSession.handshakeInfo.principal.awaitSingle() service.principal = principal @@ -567,8 +586,10 @@ actual open class KVServiceManager actual constructor(val serviceClass: noinline function: suspend T.(Int?, Int?, List?, List?, String?) -> RemoteData ) { val routeDef = "route${this::class.simpleName}${counter++}" - addRoute(HttpMethod.POST, "/kv/$routeDef") { req, ctx -> + addRoute(HttpMethod.POST, "/kv/$routeDef") { req, tlReq, ctx -> + tlReq.set(req) val service = ctx.getBean(serviceClass.java) + tlReq.remove() initializeService(service, req) val jsonRpcRequest = req.awaitBody() @Suppress("MagicNumber") @@ -623,7 +644,7 @@ actual open class KVServiceManager actual constructor(val serviceClass: fun addRoute( method: HttpMethod, path: String, - handler: suspend (ServerRequest, ApplicationContext) -> ServerResponse + handler: suspend (ServerRequest, ThreadLocal, ApplicationContext) -> ServerResponse ) { when (method) { HttpMethod.GET -> getRequests[path] = handler diff --git a/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVWebSocketConfig.kt b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVWebSocketConfig.kt index d707f6c4..e0fecb9c 100644 --- a/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVWebSocketConfig.kt +++ b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/KVWebSocketConfig.kt @@ -35,9 +35,11 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.reactor.asFlux import kotlinx.coroutines.reactor.asMono +import org.springframework.beans.factory.config.BeanDefinition 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.web.reactive.HandlerMapping import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping import org.springframework.web.reactive.socket.WebSocketHandler @@ -46,13 +48,17 @@ import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAd import reactor.core.publisher.Mono import kotlin.coroutines.EmptyCoroutineContext +/** + * Spring Boot WebSocket handler + */ class KVWebSocketHandler( private val services: List>, + private val threadLocalWebSocketSession: ThreadLocal, private val applicationContext: ApplicationContext ) : WebSocketHandler, CoroutineScope by CoroutineScope(Dispatchers.Default) { private fun getHandler(session: WebSocketSession): (suspend ( - WebSocketSession, ApplicationContext, + WebSocketSession, ThreadLocal, ApplicationContext, ReceiveChannel, SendChannel ) -> Unit) { val uri = session.handshakeInfo.uri.toString() @@ -79,7 +85,13 @@ class KVWebSocketHandler( requestChannel.close() } launch { - handler.invoke(session, applicationContext, requestChannel, responseChannel) + handler.invoke( + session, + threadLocalWebSocketSession, + applicationContext, + requestChannel, + responseChannel + ) if (!responseChannel.isClosedForReceive) responseChannel.close() session.close() } @@ -89,15 +101,25 @@ class KVWebSocketHandler( } } +/** + * Spring Boot WebSocket configuration + */ @Configuration open class KVWebSocketConfig( - private var services: List>, - private var applicationContext: ApplicationContext + private val services: List>, + private val applicationContext: ApplicationContext ) { + private val threadLocalWebSocketSession = ThreadLocal() + + @Bean + @Scope(BeanDefinition.SCOPE_PROTOTYPE) + open fun webSocketSession(): WebSocketSession { + return threadLocalWebSocketSession.get() ?: KVWebSocketSession() + } @Bean open fun handlerMapping(): HandlerMapping { - val map = mapOf("/kvws/*" to KVWebSocketHandler(services, applicationContext)) + val map = mapOf("/kvws/*" to KVWebSocketHandler(services, threadLocalWebSocketSession, applicationContext)) val order = -1 return SimpleUrlHandlerMapping(map, order) } diff --git a/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/SessionInterfaces.kt b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/SessionInterfaces.kt index 63c5a9d1..9080040a 100644 --- a/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/SessionInterfaces.kt +++ b/kvision-modules/kvision-server-spring-boot/src/jvmMain/kotlin/pl/treksoft/kvision/remote/SessionInterfaces.kt @@ -26,18 +26,22 @@ import org.springframework.web.reactive.socket.WebSocketSession import org.springframework.web.server.WebSession import java.security.Principal +@Deprecated("Use dependency injection instead.") interface WithRequest { var serverRequest: ServerRequest } +@Deprecated("Use dependency injection instead.") interface WithWebSession { var webSession: WebSession } +@Deprecated("Use dependency injection instead.") interface WithPrincipal { var principal: Principal } +@Deprecated("Use dependency injection instead.") interface WithWebSocketSession { var webSocketSession: WebSocketSession } -- cgit