/* * 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 com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.google.inject.Injector import io.jooby.Context import io.jooby.CoroutineRouter import io.jooby.HandlerContext import io.jooby.Kooby import io.jooby.WebSocketConfigurer import io.jooby.body import kotlinx.coroutines.Dispatchers 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 org.slf4j.Logger import org.slf4j.LoggerFactory import pl.treksoft.kvision.types.* import java.math.BigDecimal 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 Jooby. */ @Suppress("LargeClass", "TooManyFunctions", "BlockingMethodInNonBlockingContext") actual open class KVServiceManager actual constructor(val serviceClass: KClass) { companion object { val LOG: Logger = LoggerFactory.getLogger(KVServiceManager::class.java.name) } val getRequests: MutableMap Any> = mutableMapOf() val postRequests: MutableMap Any> = mutableMapOf() val putRequests: MutableMap Any> = mutableMapOf() val deleteRequests: MutableMap Any> = mutableMapOf() val optionsRequests: MutableMap Any> = mutableMapOf() val webSocketRequests: MutableMap Unit> = mutableMapOf() 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.addSerializer(BigDecimal::class.java, BigDecimalSerializer()) 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()) module.addDeserializer(BigDecimal::class.java, BigDecimalDeserializer()) this.registerModule(module) } var counter: Int = 0 /** * @suppress internal function */ @Suppress("DEPRECATION") fun initializeService(service: T, ctx: Context) { if (service is WithContext) { service.ctx = ctx } if (service is WithSession) { service.session = ctx.session() } } /** * Binds a given route with a function of the receiver. * @param function a function of the receiver * @param method a HTTP method * @param route a route */ @Suppress("TooGenericExceptionCaught") protected actual inline fun bind( noinline function: suspend T.() -> RET, method: HttpMethod, route: String? ) { val routeDef = route ?: "route${this::class.simpleName}${counter++}" addRoute(method, "/kv/$routeDef") { val jsonRpcRequest = if (method == HttpMethod.GET) { JsonRpcRequest(ctx.query("id").intValue(), "", listOf()) } else { ctx.body() } val injector = ctx.attribute(KV_INJECTOR_KEY) val service = injector.getInstance(serviceClass.java) initializeService(service, ctx) try { val result = function.invoke(service) JsonRpcResponse( id = jsonRpcRequest.id, result = mapper.writeValueAsString(result) ) } catch (e: Exception) { if (e !is ServiceException) LOG.error(e.message, e) JsonRpcResponse( id = jsonRpcRequest.id, error = e.message ?: "Error", exceptionType = e.javaClass.canonicalName ) } } } /** * Binds a given route with a function of the receiver. * @param function a function of the receiver * @param method a HTTP method * @param route a route */ @Suppress("TooGenericExceptionCaught") protected actual inline fun bind( noinline function: suspend T.(PAR) -> RET, method: HttpMethod, route: String? ) { 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") { val jsonRpcRequest = ctx.body() if (jsonRpcRequest.params.size == 1) { val param = getParameter(jsonRpcRequest.params[0]) val injector = ctx.attribute(KV_INJECTOR_KEY) val service = injector.getInstance(serviceClass.java) initializeService(service, ctx) try { val result = function.invoke(service, param) JsonRpcResponse( id = jsonRpcRequest.id, result = mapper.writeValueAsString(result) ) } catch (e: Exception) { if (e !is ServiceException) LOG.error(e.message, e) JsonRpcResponse( id = jsonRpcRequest.id, error = e.message ?: "Error", exceptionType = e.javaClass.canonicalName ) } } else { JsonRpcResponse(id = jsonRpcRequest.id, error = "Invalid parameters") } } } /** * Binds a given route with a function of the receiver. * @param function a function of the receiver * @param method a HTTP method * @param route a route */ @Suppress("TooGenericExceptionCaught") protected actual inline fun bind( noinline function: suspend T.(PAR1, PAR2) -> RET, method: HttpMethod, route: String? ) { 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") { val jsonRpcRequest = ctx.body() if (jsonRpcRequest.params.size == 2) { val param1 = getParameter(jsonRpcRequest.params[0]) val param2 = getParameter(jsonRpcRequest.params[1]) val injector = ctx.attribute(KV_INJECTOR_KEY) val service = injector.getInstance(serviceClass.java) initializeService(service, ctx) try { val result = function.invoke(service, param1, param2) JsonRpcResponse( id = jsonRpcRequest.id, result = mapper.writeValueAsString(result) ) } catch (e: Exception) { if (e !is ServiceException) LOG.error(e.message, e) JsonRpcResponse( id = jsonRpcRequest.id, error = e.message ?: "Error", exceptionType = e.javaClass.canonicalName ) } } else { JsonRpcResponse(id = jsonRpcRequest.id, error = "Invalid parameters") } } } /** * Binds a given route with a function of the receiver. * @param function a function of the receiver * @param method a HTTP method * @param route a route */ @Suppress("TooGenericExceptionCaught") protected actual inline fun bind( noinline function: suspend T.(PAR1, PAR2, PAR3) -> RET, method: HttpMethod, route: String? ) { 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") { val jsonRpcRequest = ctx.body() @Suppress("MagicNumber") if (jsonRpcRequest.params.size == 3) { val param1 = getParameter(jsonRpcRequest.params[0]) val param2 = getParameter(jsonRpcRequest.params[1]) val param3 = getParameter(jsonRpcRequest.params[2]) val injector = ctx.attribute(KV_INJECTOR_KEY) val service = injector.getInstance(serviceClass.java) initializeService(service, ctx) try { val result = function.invoke(service, param1, param2, param3) JsonRpcResponse( id = jsonRpcRequest.id, result = mapper.writeValueAsString(result) ) } catch (e: Exception) { if (e !is ServiceException) LOG.error(e.message, e) JsonRpcResponse( id = jsonRpcRequest.id, error = e.message ?: "Error", exceptionType = e.javaClass.canonicalName ) } } else { JsonRpcResponse(id = jsonRpcRequest.id, error = "Invalid parameters") } } } /** * Binds a given route with a function of the receiver. * @param function a function of the receiver * @param method a HTTP method * @param route a route */ @Suppress("TooGenericExceptionCaught") protected actual inline fun bind( noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4) -> RET, method: HttpMethod, route: String? ) { 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") { val jsonRpcRequest = ctx.body() @Suppress("MagicNumber") if (jsonRpcRequest.params.size == 4) { val param1 = getParameter(jsonRpcRequest.params[0]) val param2 = getParameter(jsonRpcRequest.params[1]) val param3 = getParameter(jsonRpcRequest.params[2]) val param4 = getParameter(jsonRpcRequest.params[3]) val injector = ctx.attribute(KV_INJECTOR_KEY) val service = injector.getInstance(serviceClass.java) initializeService(service, ctx) try { val result = function.invoke(service, param1, param2, param3, param4) JsonRpcResponse( id = jsonRpcRequest.id, result = mapper.writeValueAsString(result) ) } catch (e: Exception) { if (e !is ServiceException) LOG.error(e.message, e) JsonRpcResponse( id = jsonRpcRequest.id, error = e.message ?: "Error", exceptionType = e.javaClass.canonicalName ) } } else { JsonRpcResponse(id = jsonRpcRequest.id, error = "Invalid parameters") } } } /** * Binds a given route with a function of the receiver. * @param function a function of the receiver * @param method a HTTP method * @param route a route */ @Suppress("TooGenericExceptionCaught") protected actual inline fun bind( noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4, PAR5) -> RET, method: HttpMethod, route: String? ) { 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") { val jsonRpcRequest = ctx.body() @Suppress("MagicNumber") if (jsonRpcRequest.params.size == 5) { val param1 = getParameter(jsonRpcRequest.params[0]) val param2 = getParameter(jsonRpcRequest.params[1]) val param3 = getParameter(jsonRpcRequest.params[2]) val param4 = getParameter(jsonRpcRequest.params[3]) val param5 = getParameter(jsonRpcRequest.params[4]) val injector = ctx.attribute(KV_INJECTOR_KEY) val service = injector.getInstance(serviceClass.java) initializeService(service, ctx) try { val result = function.invoke(service, param1, param2, param3, param4, param5) JsonRpcResponse( id = jsonRpcRequest.id, result = mapper.writeValueAsString(result) ) } catch (e: Exception) { if (e !is ServiceException) LOG.error(e.message, e) JsonRpcResponse( id = jsonRpcRequest.id, error = e.message ?: "Error", exceptionType = e.javaClass.canonicalName ) } } else { JsonRpcResponse(id = jsonRpcRequest.id, error = "Invalid parameters") } } } /** * Binds a given route with a function of the receiver. * @param function a function of the receiver * @param method a HTTP method * @param route a route */ @Suppress("TooGenericExceptionCaught") protected actual inline fun bind( noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4, PAR5, PAR6) -> RET, method: HttpMethod, route: String? ) { 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") { val jsonRpcRequest = ctx.body() @Suppress("MagicNumber") if (jsonRpcRequest.params.size == 6) { val param1 = getParameter(jsonRpcRequest.params[0]) val param2 = getParameter(jsonRpcRequest.params[1]) val param3 = getParameter(jsonRpcRequest.params[2]) val param4 = getParameter(jsonRpcRequest.params[3]) val param5 = getParameter(jsonRpcRequest.params[4]) val param6 = getParameter(jsonRpcRequest.params[5]) val injector = ctx.attribute(KV_INJECTOR_KEY) val service = injector.getInstance(serviceClass.java) initializeService(service, ctx) try { val result = function.invoke(service, param1, param2, param3, param4, param5, param6) JsonRpcResponse( id = jsonRpcRequest.id, result = mapper.writeValueAsString(result) ) } catch (e: Exception) { if (e !is ServiceException) LOG.error(e.message, e) JsonRpcResponse( id = jsonRpcRequest.id, error = e.message ?: "Error", exceptionType = e.javaClass.canonicalName ) } } else { JsonRpcResponse(id = jsonRpcRequest.id, error = "Invalid parameters") } } } /** * Binds a given web socket connection with a function of the receiver. * @param function a function of the receiver * @param route a route */ @OptIn(ExperimentalCoroutinesApi::class) protected actual inline fun bind( noinline function: suspend T.(ReceiveChannel, SendChannel) -> Unit, route: String? ) { val routeDef = route ?: "route${this::class.simpleName}${counter++}" webSocketRequests["/kvws/$routeDef"] = { ctx, configurer -> val injector = ctx.require(Injector::class.java).createChildInjector(ContextModule(ctx)) val service = injector.getInstance(serviceClass.java) initializeService(service, ctx) val incoming = Channel() val outgoing = Channel() configurer.onConnect { ws -> GlobalScope.launch { coroutineScope { launch(Dispatchers.IO) { for (text in outgoing) { ws.send(text) } ws.close() } launch { val requestChannel = Channel() val responseChannel = Channel() coroutineScope { launch { for (p in incoming) { val jsonRpcRequest = getParameter(p) if (jsonRpcRequest.params.size == 1) { val par = getParameter(jsonRpcRequest.params[0]) requestChannel.send(par) } } requestChannel.close() } launch(Dispatchers.IO) { for (p in responseChannel) { val text = mapper.writeValueAsString( JsonRpcResponse( id = 0, result = mapper.writeValueAsString(p) ) ) outgoing.send(text) } } launch { function.invoke(service, requestChannel, responseChannel) if (!responseChannel.isClosedForReceive) responseChannel.close() } } if (!outgoing.isClosedForReceive) outgoing.close() } } } } configurer.onClose { _, _ -> GlobalScope.launch { outgoing.close() incoming.close() } } configurer.onMessage { _, msg -> GlobalScope.launch { incoming.send(msg.value()) } } } } /** * Binds a given function of the receiver as a tabulator component source * @param function a function of the receiver */ @Suppress("TooGenericExceptionCaught") protected actual inline fun bindTabulatorRemote( noinline function: suspend T.(Int?, Int?, List?, List?, String?) -> RemoteData, route: String? ) { val routeDef = route ?: "route${this::class.simpleName}${counter++}" addRoute(HttpMethod.POST, "/kv/$routeDef") { val jsonRpcRequest = ctx.body() @Suppress("MagicNumber") if (jsonRpcRequest.params.size == 5) { val param1 = getParameter(jsonRpcRequest.params[0]) val param2 = getParameter(jsonRpcRequest.params[1]) val param3 = getParameter?>(jsonRpcRequest.params[2]) @Suppress("MagicNumber") val param4 = getParameter?>(jsonRpcRequest.params[3]) @Suppress("MagicNumber") val param5 = getParameter(jsonRpcRequest.params[4]) val injector = ctx.attribute(KV_INJECTOR_KEY) val service = injector.getInstance(serviceClass.java) initializeService(service, ctx) try { val result = function.invoke(service, param1, param2, param3, param4, param5) JsonRpcResponse( id = jsonRpcRequest.id, result = mapper.writeValueAsString(result) ) } catch (e: Exception) { if (e !is ServiceException) LOG.error(e.message, e) JsonRpcResponse( id = jsonRpcRequest.id, error = e.message ?: "Error", exceptionType = e.javaClass.canonicalName ) } } else { JsonRpcResponse(id = jsonRpcRequest.id, error = "Invalid parameters") } } } /** * @suppress Internal method */ fun addRoute( method: HttpMethod, path: String, handler: suspend HandlerContext.() -> Any ) { when (method) { HttpMethod.GET -> getRequests[path] = handler HttpMethod.POST -> postRequests[path] = handler HttpMethod.PUT -> putRequests[path] = handler HttpMethod.DELETE -> deleteRequests[path] = handler HttpMethod.OPTIONS -> optionsRequests[path] = handler } } /** * @suppress Internal method */ protected inline fun getParameter(str: String?): T { return str?.let { if (T::class == String::class) { str as T } else { mapper.readValue(str) } } ?: null as T } } /** * A function to generate routes based on definitions from the service manager. */ fun CoroutineRouter.applyRoutes(serviceManager: KVServiceManager) { serviceManager.getRequests.forEach { (path, handler) -> get(path, handler) } serviceManager.postRequests.forEach { (path, handler) -> post(path, handler) } serviceManager.putRequests.forEach { (path, handler) -> put(path, handler) } serviceManager.deleteRequests.forEach { (path, handler) -> delete(path, handler) } serviceManager.optionsRequests.forEach { (path, handler) -> options(path, handler) } serviceManager.webSocketRequests.forEach { (path, handler) -> this.router.ws(path, handler) } } /** * A function to generate routes based on definitions from the service manager. */ fun Kooby.applyRoutes(serviceManager: KVServiceManager) { coroutine { applyRoutes(serviceManager) } }