/* * 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.javalin.Javalin import io.javalin.core.security.Role import io.javalin.http.Context import io.javalin.websocket.WsContext import io.javalin.websocket.WsHandler 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.future.future 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 Javalin. */ @Suppress("LargeClass", "TooManyFunctions", "BlockingMethodInNonBlockingContext") actual open class KVServiceManager actual constructor(val serviceClass: KClass) { companion object { val LOG: Logger = LoggerFactory.getLogger(KVServiceManager::class.java.name) const val KV_WS_INCOMING_KEY = "pl.treksoft.kvision.ws.incoming.key" const val KV_WS_OUTGOING_KEY = "pl.treksoft.kvision.ws.outgoing.key" } val getRequests: MutableMap Unit> = mutableMapOf() val postRequests: MutableMap Unit> = mutableMapOf() val putRequests: MutableMap Unit> = mutableMapOf() val deleteRequests: MutableMap Unit> = mutableMapOf() val optionsRequests: MutableMap Unit> = 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 WithHttpSession) { service.httpSession = ctx.req.session } } /** * @suppress internal function */ @Suppress("DEPRECATION") fun initializeWsService(service: T, wsCtx: WsContext) { if (service is WithWsContext) { service.wsCtx = wsCtx } if (service is WithWsSession) { service.wsSession = wsCtx.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") { ctx -> val jsonRpcRequest = if (method == HttpMethod.GET) { JsonRpcRequest(ctx.queryParam("id").get(), "", listOf()) } else { ctx.body() } val injector = ctx.attribute(KV_INJECTOR_KEY)!! val service = injector.getInstance(serviceClass.java) initializeService(service, ctx) val future = GlobalScope.future { 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 ) } } ctx.json(future) } } /** * 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") { ctx -> 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) val future = GlobalScope.future { 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 ) } } ctx.json(future) } else { ctx.json(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") { ctx -> 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) val future = GlobalScope.future { 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 ) } } ctx.json(future) } else { ctx.json(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") { ctx -> 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) val future = GlobalScope.future { 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 ) } } ctx.json(future) } else { ctx.json(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") { ctx -> 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) val future = GlobalScope.future { 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 ) } } ctx.json(future) } else { ctx.json(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") { ctx -> 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) val future = GlobalScope.future { 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 ) } } ctx.json(future) } else { ctx.json(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") { ctx -> 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) val future = GlobalScope.future { 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 ) } } ctx.json(future) } else { ctx.json(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"] = { ws -> ws.onConnect { ctx -> val incoming = Channel() val outgoing = Channel() ctx.attribute(KV_WS_INCOMING_KEY, incoming) ctx.attribute(KV_WS_OUTGOING_KEY, outgoing) val injector = ctx.attribute(KV_INJECTOR_KEY)!! val service = injector.getInstance(serviceClass.java) initializeWsService(service, ctx) GlobalScope.launch { coroutineScope { launch(Dispatchers.IO) { for (text in outgoing) { ctx.send(text) } ctx.session.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() } } } } ws.onClose { ctx -> GlobalScope.launch { val outgoing = ctx.attribute>(KV_WS_OUTGOING_KEY)!! val incoming = ctx.attribute>(KV_WS_INCOMING_KEY)!! outgoing.close() incoming.close() } } ws.onMessage { ctx -> GlobalScope.launch { val incoming = ctx.attribute>(KV_WS_INCOMING_KEY)!! incoming.send(ctx.message()) } } } } /** * 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") { ctx -> 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) val future = GlobalScope.future { 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 ) } } ctx.json(future) } else { ctx.json(JsonRpcResponse(id = jsonRpcRequest.id, error = "Invalid parameters")) } } } /** * @suppress Internal method */ fun addRoute( method: HttpMethod, path: String, handler: (Context) -> Unit ) { 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 Javalin.applyRoutes(serviceManager: KVServiceManager, roles: Set = setOf()) { serviceManager.getRequests.forEach { (path, handler) -> get(path, handler, roles) } serviceManager.postRequests.forEach { (path, handler) -> post(path, handler, roles) } serviceManager.putRequests.forEach { (path, handler) -> put(path, handler, roles) } serviceManager.deleteRequests.forEach { (path, handler) -> delete(path, handler, roles) } serviceManager.optionsRequests.forEach { (path, handler) -> options(path, handler, roles) } serviceManager.webSocketRequests.forEach { (path, handler) -> ws(path, handler, roles) } }