diff options
Diffstat (limited to 'kvision-modules/kvision-server-spring-boot/src/jvmMain')
5 files changed, 279 insertions, 24 deletions
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 <T : Any?> body(extractor: BodyExtractor<T, in ServerHttpRequest>): T { + throw IllegalStateException("Empty implementation") + } + + override fun <T : Any?> body(extractor: BodyExtractor<T, in ServerHttpRequest>, hints: MutableMap<String, Any>): T { + throw IllegalStateException("Empty implementation") + } + + override fun remoteAddress(): Optional<InetSocketAddress> { + throw IllegalStateException("Empty implementation") + } + + override fun attributes(): MutableMap<String, Any> { + throw IllegalStateException("Empty implementation") + } + + override fun session(): Mono<WebSession> { + throw IllegalStateException("Empty implementation") + } + + override fun principal(): Mono<out Principal> { + throw IllegalStateException("Empty implementation") + } + + override fun multipartData(): Mono<MultiValueMap<String, Part>> { + throw IllegalStateException("Empty implementation") + } + + override fun messageReaders(): MutableList<HttpMessageReader<*>> { + throw IllegalStateException("Empty implementation") + } + + override fun uri(): URI { + throw IllegalStateException("Empty implementation") + } + + override fun exchange(): ServerWebExchange { + throw IllegalStateException("Empty implementation") + } + + override fun <T : Any?> bodyToFlux(elementClass: Class<out T>): Flux<T> { + throw IllegalStateException("Empty implementation") + } + + override fun <T : Any?> bodyToFlux(typeReference: ParameterizedTypeReference<T>): Flux<T> { + throw IllegalStateException("Empty implementation") + } + + override fun formData(): Mono<MultiValueMap<String, String>> { + throw IllegalStateException("Empty implementation") + } + + override fun queryParams(): MultiValueMap<String, String> { + throw IllegalStateException("Empty implementation") + } + + override fun pathVariables(): MutableMap<String, String> { + throw IllegalStateException("Empty implementation") + } + + override fun localAddress(): Optional<InetSocketAddress> { + throw IllegalStateException("Empty implementation") + } + + override fun uriBuilder(): UriBuilder { + throw IllegalStateException("Empty implementation") + } + + override fun methodName(): String { + throw IllegalStateException("Empty implementation") + } + + override fun <T : Any?> bodyToMono(elementClass: Class<out T>): Mono<T> { + throw IllegalStateException("Empty implementation") + } + + override fun <T : Any?> bodyToMono(typeReference: ParameterizedTypeReference<T>): Mono<T> { + throw IllegalStateException("Empty implementation") + } + + override fun cookies(): MultiValueMap<String, HttpCookie> { + 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<DataBufferFactory, DataBuffer>): WebSocketMessage { + throw IllegalStateException("Empty implementation") + } + + override fun pingMessage(payloadFactory: Function<DataBufferFactory, DataBuffer>): WebSocketMessage { + throw IllegalStateException("Empty implementation") + } + + override fun bufferFactory(): DataBufferFactory { + throw IllegalStateException("Empty implementation") + } + + override fun getAttributes(): MutableMap<String, Any> { + throw IllegalStateException("Empty implementation") + } + + override fun receive(): Flux<WebSocketMessage> { + throw IllegalStateException("Empty implementation") + } + + override fun pongMessage(payloadFactory: Function<DataBufferFactory, DataBuffer>): WebSocketMessage { + throw IllegalStateException("Empty implementation") + } + + override fun getHandshakeInfo(): HandshakeInfo { + throw IllegalStateException("Empty implementation") + } + + override fun send(messages: Publisher<WebSocketMessage>): Mono<Void> { + throw IllegalStateException("Empty implementation") + } + + override fun close(status: CloseStatus): Mono<Void> { + 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<KVServiceManager<*>>, var applicationContext: ApplicationContext) { +open class KVHandler(val services: List<KVServiceManager<*>>, val applicationContext: ApplicationContext) { + + private val threadLocalRequest = ThreadLocal<ServerRequest>() + + @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<KVServiceManager<*>>, 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<T : Any> actual constructor(val serviceClass: val LOG: Logger = LoggerFactory.getLogger(KVServiceManager::class.java.name) } - val getRequests: MutableMap<String, suspend (ServerRequest, ApplicationContext) -> ServerResponse> = + val getRequests: MutableMap<String, suspend (ServerRequest, ThreadLocal<ServerRequest>, ApplicationContext) -> ServerResponse> = mutableMapOf() - val postRequests: MutableMap<String, suspend (ServerRequest, ApplicationContext) -> ServerResponse> = + val postRequests: MutableMap<String, suspend (ServerRequest, ThreadLocal<ServerRequest>, ApplicationContext) -> ServerResponse> = mutableMapOf() - val putRequests: MutableMap<String, suspend (ServerRequest, ApplicationContext) -> ServerResponse> = + val putRequests: MutableMap<String, suspend (ServerRequest, ThreadLocal<ServerRequest>, ApplicationContext) -> ServerResponse> = mutableMapOf() - val deleteRequests: MutableMap<String, suspend (ServerRequest, ApplicationContext) -> ServerResponse> = + val deleteRequests: MutableMap<String, suspend (ServerRequest, ThreadLocal<ServerRequest>, ApplicationContext) -> ServerResponse> = mutableMapOf() - val optionsRequests: MutableMap<String, suspend (ServerRequest, ApplicationContext) -> ServerResponse> = + val optionsRequests: MutableMap<String, suspend (ServerRequest, ThreadLocal<ServerRequest>, ApplicationContext) -> ServerResponse> = mutableMapOf() val webSocketsRequests: MutableMap<String, suspend ( - WebSocketSession, ApplicationContext, ReceiveChannel<String>, SendChannel<String> + WebSocketSession, ThreadLocal<WebSocketSession>, ApplicationContext, ReceiveChannel<String>, SendChannel<String> ) -> Unit> = mutableMapOf() @@ -95,6 +95,7 @@ actual open class KVServiceManager<T : Any> 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<T : Any> 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<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, 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<JsonRpcRequest>() if (jsonRpcRequest.params.size == 1) { @@ -223,8 +228,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, 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<JsonRpcRequest>() if (jsonRpcRequest.params.size == 2) { @@ -279,8 +286,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, 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<JsonRpcRequest>() @Suppress("MagicNumber") @@ -337,8 +346,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, 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<JsonRpcRequest>() @Suppress("MagicNumber") @@ -397,8 +408,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, 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<JsonRpcRequest>() @Suppress("MagicNumber") @@ -458,8 +471,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, 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<JsonRpcRequest>() @Suppress("MagicNumber") @@ -516,11 +531,15 @@ actual open class KVServiceManager<T : Any> 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<T : Any> actual constructor(val serviceClass: noinline function: suspend T.(Int?, Int?, List<RemoteFilter>?, List<RemoteSorter>?, String?) -> RemoteData<RET> ) { 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<JsonRpcRequest>() @Suppress("MagicNumber") @@ -623,7 +644,7 @@ actual open class KVServiceManager<T : Any> actual constructor(val serviceClass: fun addRoute( method: HttpMethod, path: String, - handler: suspend (ServerRequest, ApplicationContext) -> ServerResponse + handler: suspend (ServerRequest, ThreadLocal<ServerRequest>, 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<KVServiceManager<*>>, + private val threadLocalWebSocketSession: ThreadLocal<WebSocketSession>, private val applicationContext: ApplicationContext ) : WebSocketHandler, CoroutineScope by CoroutineScope(Dispatchers.Default) { private fun getHandler(session: WebSocketSession): (suspend ( - WebSocketSession, ApplicationContext, + WebSocketSession, ThreadLocal<WebSocketSession>, ApplicationContext, ReceiveChannel<String>, SendChannel<String> ) -> 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<KVServiceManager<*>>, - private var applicationContext: ApplicationContext + private val services: List<KVServiceManager<*>>, + private val applicationContext: ApplicationContext ) { + private val threadLocalWebSocketSession = ThreadLocal<WebSocketSession>() + + @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 } |