aboutsummaryrefslogtreecommitdiff
path: root/kvision-modules/kvision-remote/src
diff options
context:
space:
mode:
Diffstat (limited to 'kvision-modules/kvision-remote/src')
-rw-r--r--kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/CallAgent.kt135
-rw-r--r--kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/JoobyRemoteAgent.kt370
-rw-r--r--kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/JoobyServiceManager.kt129
-rw-r--r--kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/KVServer.kt123
-rw-r--r--kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt208
-rw-r--r--kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/Security.kt116
-rw-r--r--kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/SpringRemoteAgent.kt370
-rw-r--r--kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/SpringServiceManager.kt124
-rw-r--r--kvision-modules/kvision-remote/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt99
-rw-r--r--kvision-modules/kvision-remote/src/test/kotlin/test/pl/treksoft/kvision/form/upload/UploadInputSpec.kt57
-rw-r--r--kvision-modules/kvision-remote/src/test/kotlin/test/pl/treksoft/kvision/form/upload/UploadSpec.kt56
11 files changed, 1787 insertions, 0 deletions
diff --git a/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/CallAgent.kt b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/CallAgent.kt
new file mode 100644
index 00000000..4a086e2a
--- /dev/null
+++ b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/CallAgent.kt
@@ -0,0 +1,135 @@
+/*
+ * 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 kotlinx.serialization.ImplicitReflectionSerializer
+import kotlinx.serialization.stringify
+import pl.treksoft.jquery.JQueryAjaxSettings
+import pl.treksoft.jquery.JQueryXHR
+import pl.treksoft.jquery.jQuery
+import pl.treksoft.kvision.utils.JSON
+import pl.treksoft.kvision.utils.obj
+import kotlin.js.Promise
+import kotlin.js.undefined
+import kotlin.js.JSON as NativeJSON
+
+/**
+ * HTTP status unauthorized (401).
+ */
+const val HTTP_UNAUTHORIZED = 401
+
+/**
+ * An agent responsible for remote calls.
+ */
+open class CallAgent {
+
+ private var counter = 1
+
+ /**
+ * Makes an JSON-RPC call to the remote server.
+ * @param url an URL address
+ * @param method a HTTP method
+ * @param data data to be sent
+ * @return a promise of the result
+ */
+ @UseExperimental(ImplicitReflectionSerializer::class)
+ @Suppress("UnsafeCastFromDynamic")
+ fun jsonRpcCall(
+ url: String,
+ data: List<String?> = listOf(),
+ method: RpcHttpMethod = RpcHttpMethod.POST
+ ): Promise<String> {
+ val jsonRpcRequest = JsonRpcRequest(counter++, url, data)
+ val jsonData = JSON.plain.stringify(jsonRpcRequest)
+ return Promise { resolve, reject ->
+ jQuery.ajax(url, obj {
+ this.contentType = "application/json"
+ this.data = jsonData
+ this.method = method.name
+ this.success =
+ { data: dynamic, _: Any, _: Any ->
+ when {
+ data.id != jsonRpcRequest.id -> reject(Exception("Invalid response ID"))
+ data.error != null -> reject(Exception(data.error.toString()))
+ data.result != null -> resolve(data.result)
+ else -> reject(Exception("Invalid response"))
+ }
+ }
+ this.error =
+ { xhr: JQueryXHR, _: String, errorText: String ->
+ val message = if (xhr.responseJSON != null && xhr.responseJSON != undefined) {
+ xhr.responseJSON.toString()
+ } else {
+ errorText
+ }
+ if (xhr.status.toInt() == HTTP_UNAUTHORIZED) {
+ reject(SecurityException(message))
+ } else {
+ reject(Exception(message))
+ }
+ }
+ })
+ }
+ }
+
+ /**
+ * Makes a remote call to the remote server.
+ * @param url an URL address
+ * @param method a HTTP method
+ * @param data data to be sent
+ * @return a promise of the result
+ */
+ @Suppress("UnsafeCastFromDynamic")
+ fun remoteCall(
+ url: String,
+ data: dynamic = null,
+ method: HttpMethod = HttpMethod.GET,
+ contentType: String = "application/json",
+ beforeSend: ((JQueryXHR, JQueryAjaxSettings) -> Boolean)? = null
+ ): Promise<dynamic> {
+ return Promise { resolve, reject ->
+ jQuery.ajax(url, obj {
+ this.contentType = contentType
+ this.data = data
+ this.method = method.name
+ this.success =
+ { data: dynamic, _: Any, _: Any ->
+ resolve(data)
+ }
+ this.error =
+ { xhr: JQueryXHR, _: String, errorText: String ->
+ val message = if (xhr.responseJSON != null && xhr.responseJSON != undefined) {
+ xhr.responseJSON.toString()
+ } else {
+ errorText
+ }
+ if (xhr.status.toInt() == HTTP_UNAUTHORIZED) {
+ reject(SecurityException(message))
+ } else {
+ reject(Exception(message))
+ }
+ }
+ this.beforeSend = beforeSend
+ })
+ }
+ }
+}
diff --git a/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/JoobyRemoteAgent.kt b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/JoobyRemoteAgent.kt
new file mode 100644
index 00000000..318f77ea
--- /dev/null
+++ b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/JoobyRemoteAgent.kt
@@ -0,0 +1,370 @@
+/*
+ * 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 kotlinx.coroutines.asDeferred
+import kotlinx.serialization.ImplicitReflectionSerializer
+import kotlinx.serialization.list
+import kotlinx.serialization.serializer
+import pl.treksoft.kvision.utils.JSON
+import kotlin.js.js
+import kotlin.reflect.KClass
+import kotlin.js.JSON as NativeJSON
+
+/**
+ * Client side agent for JSON-RPC remote calls with Jooby.
+ */
+@Suppress("LargeClass", "TooManyFunctions")
+@UseExperimental(ImplicitReflectionSerializer::class)
+open class JoobyRemoteAgent<T : Any>(val serviceManager: JoobyServiceManager<T>) : RemoteAgent {
+
+ val callAgent = CallAgent()
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified RET : Any, T> call(noinline function: suspend T.(Request?) -> RET): RET {
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, method = method).then {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ deserialize<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnum(RET::class as KClass<Any>, it) as RET
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer(), it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified RET : Any, T> call(
+ noinline function: suspend T.(Request?) -> List<RET>
+ ): List<RET> {
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, method = method).then {
+ try {
+ deserializeList<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnumList(RET::class as KClass<Any>, it) as List<RET>
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer().list, it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR, Request?) -> RET, p: PAR
+ ): RET {
+ val data = serialize(p)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data), method).then {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ deserialize<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnum(RET::class as KClass<Any>, it) as RET
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer(), it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR, Request?) -> List<RET>, p: PAR
+ ): List<RET> {
+ val data = serialize(p)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data), method).then {
+ try {
+ deserializeList<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnumList(RET::class as KClass<Any>, it) as List<RET>
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer().list, it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR1, reified PAR2, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2, Request?) -> RET, p1: PAR1, p2: PAR2
+ ): RET {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2), method).then {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ deserialize<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnum(RET::class as KClass<Any>, it) as RET
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer(), it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR1, reified PAR2, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2, Request?) -> List<RET>, p1: PAR1, p2: PAR2
+ ): List<RET> {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2), method).then {
+ try {
+ deserializeList<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnumList(RET::class as KClass<Any>, it) as List<RET>
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer().list, it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR1, reified PAR2, reified PAR3, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2, PAR3, Request?) -> RET, p1: PAR1, p2: PAR2, p3: PAR3
+ ): RET {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val data3 = serialize(p3)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2, data3), method).then {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ deserialize<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnum(RET::class as KClass<Any>, it) as RET
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer(), it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR1, reified PAR2, reified PAR3, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2, PAR3, Request?) -> List<RET>, p1: PAR1, p2: PAR2, p3: PAR3
+ ): List<RET> {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val data3 = serialize(p3)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2, data3), method).then {
+ try {
+ deserializeList<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnumList(RET::class as KClass<Any>, it) as List<RET>
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer().list, it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4, Request?) -> RET, p1: PAR1, p2: PAR2, p3: PAR3, p4: PAR4
+ ): RET {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val data3 = serialize(p3)
+ val data4 = serialize(p4)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2, data3, data4), method).then {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ deserialize<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnum(RET::class as KClass<Any>, it) as RET
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer(), it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4, Request?) -> List<RET>,
+ p1: PAR1,
+ p2: PAR2,
+ p3: PAR3,
+ p4: PAR4
+ ): List<RET> {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val data3 = serialize(p3)
+ val data4 = serialize(p4)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2, data3, data4), method).then {
+ try {
+ deserializeList<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnumList(RET::class as KClass<Any>, it) as List<RET>
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer().list, it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ @Suppress("LongParameterList")
+ suspend inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified PAR5,
+ reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4, PAR5, Request?) -> RET,
+ p1: PAR1,
+ p2: PAR2,
+ p3: PAR3,
+ p4: PAR4,
+ p5: PAR5
+ ): RET {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val data3 = serialize(p3)
+ val data4 = serialize(p4)
+ val data5 = serialize(p5)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2, data3, data4, data5), method).then {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ deserialize<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnum(RET::class as KClass<Any>, it) as RET
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer(), it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ @Suppress("LongParameterList")
+ suspend inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified PAR5,
+ reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4, PAR5, Request?) -> List<RET>,
+ p1: PAR1,
+ p2: PAR2,
+ p3: PAR3,
+ p4: PAR4,
+ p5: PAR5
+ ): List<RET> {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val data3 = serialize(p3)
+ val data4 = serialize(p4)
+ val data5 = serialize(p5)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2, data3, data4, data5), method).then {
+ try {
+ deserializeList<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnumList(RET::class as KClass<Any>, it) as List<RET>
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer().list, it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+
+ /**
+ * @suppress
+ * Internal function
+ */
+ inline fun <reified PAR> serialize(value: PAR): String? {
+ return value?.let {
+ @Suppress("UNCHECKED_CAST")
+ trySerialize((PAR::class as KClass<Any>), it as Any)
+ }
+ }
+
+}
diff --git a/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/JoobyServiceManager.kt b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/JoobyServiceManager.kt
new file mode 100644
index 00000000..0d3515a1
--- /dev/null
+++ b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/JoobyServiceManager.kt
@@ -0,0 +1,129 @@
+/*
+ * 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
+
+/**
+ * Multiplatform service manager for Jooby.
+ */
+actual open class JoobyServiceManager<T : Any> actual constructor(service: T) : ServiceManager {
+
+ protected val calls: MutableMap<String, Pair<String, RpcHttpMethod>> = mutableMapOf()
+ var counter: Int = 0
+
+ /**
+ * Binds a given route with a function of the receiver.
+ * @param function a function of the receiver
+ * @param route a route
+ * @param method a HTTP method
+ */
+ protected actual inline fun <reified RET> bind(
+ noinline function: suspend T.(Request?) -> RET,
+ route: String?, method: RpcHttpMethod
+ ) {
+ val routeDef = route ?: "route${this::class.simpleName}${counter++}"
+ calls[function.toString()] = Pair("/kv/$routeDef", method)
+ }
+
+ /**
+ * Binds a given route with a function of the receiver.
+ * @param function a function of the receiver
+ * @param route a route
+ * @param method a HTTP method
+ */
+ protected actual inline fun <reified PAR, reified RET> bind(
+ noinline function: suspend T.(PAR, Request?) -> RET,
+ route: String?, method: RpcHttpMethod
+ ) {
+ val routeDef = route ?: "route${this::class.simpleName}${counter++}"
+ calls[function.toString()] = Pair("/kv/$routeDef", method)
+ }
+
+ /**
+ * Binds a given route with a function of the receiver.
+ * @param function a function of the receiver
+ * @param route a route
+ * @param method a HTTP method
+ */
+ protected actual inline fun <reified PAR1, reified PAR2, reified RET> bind(
+ noinline function: suspend T.(PAR1, PAR2, Request?) -> RET,
+ route: String?, method: RpcHttpMethod
+ ) {
+ val routeDef = route ?: "route${this::class.simpleName}${counter++}"
+ calls[function.toString()] = Pair("/kv/$routeDef", method)
+ }
+
+ /**
+ * Binds a given route with a function of the receiver.
+ * @param function a function of the receiver
+ * @param route a route
+ * @param method a HTTP method
+ */
+ protected actual inline fun <reified PAR1, reified PAR2, reified PAR3, reified RET> bind(
+ noinline function: suspend T.(PAR1, PAR2, PAR3, Request?) -> RET,
+ route: String?, method: RpcHttpMethod
+ ) {
+ val routeDef = route ?: "route${this::class.simpleName}${counter++}"
+ calls[function.toString()] = Pair("/kv/$routeDef", method)
+ }
+
+ /**
+ * Binds a given route with a function of the receiver.
+ * @param function a function of the receiver
+ * @param route a route
+ * @param method a HTTP method
+ */
+ protected actual inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified RET> bind(
+ noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4, Request?) -> RET,
+ route: String?, method: RpcHttpMethod
+ ) {
+ val routeDef = route ?: "route${this::class.simpleName}${counter++}"
+ calls[function.toString()] = Pair("/kv/$routeDef", method)
+ }
+
+ /**
+ * Binds a given route with a function of the receiver.
+ * @param function a function of the receiver
+ * @param route a route
+ * @param method a HTTP method
+ */
+ protected actual inline fun <reified PAR1, reified PAR2, reified PAR3,
+ reified PAR4, reified PAR5, reified RET> bind(
+ noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4, PAR5, Request?) -> RET,
+ route: String?,
+ method: RpcHttpMethod
+ ) {
+ val routeDef = route ?: "route${this::class.simpleName}${counter++}"
+ calls[function.toString()] = Pair("/kv/$routeDef", method)
+ }
+
+ /**
+ * Applies all defined routes to the given server.
+ * Not used on the js platform.
+ */
+ actual fun applyRoutes(k: KVServer) {
+ }
+
+ /**
+ * Returns the map of defined paths.
+ */
+ override fun getCalls(): Map<String, Pair<String, RpcHttpMethod>> = calls
+}
diff --git a/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/KVServer.kt b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/KVServer.kt
new file mode 100644
index 00000000..a1cc40ca
--- /dev/null
+++ b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/KVServer.kt
@@ -0,0 +1,123 @@
+/*
+ * 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 kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+
+/**
+ * A server.
+ * Not used on the js platform.
+ */
+actual open class KVServer
+
+/**
+ * A server request.
+ * Not used on the js platform.
+ */
+actual interface Request
+
+/**
+ * A user profile.
+ */
+@Serializable
+actual data class Profile(
+ val id: String? = null,
+ val attributes: MutableMap<String, String> = mutableMapOf(),
+ val authenticationAttributes: MutableMap<String, String> = mutableMapOf(),
+ val roles: MutableSet<String> = mutableSetOf(),
+ val permissions: MutableSet<String> = mutableSetOf(),
+ val linkedId: String? = null,
+ val remembered: Boolean = false,
+ val clientName: String? = null
+) {
+ @Transient
+ var username: String?
+ get() = attributes["username"]
+ set(value) {
+ if (value != null) {
+ attributes["username"] = value
+ } else {
+ attributes.remove("username")
+ }
+ }
+ @Transient
+ var firstName: String?
+ get() = attributes["first_name"]
+ set(value) {
+ if (value != null) {
+ attributes["first_name"] = value
+ } else {
+ attributes.remove("first_name")
+ }
+ }
+ @Transient
+ var familyName: String?
+ get() = attributes["family_name"]
+ set(value) {
+ if (value != null) {
+ attributes["family_name"] = value
+ } else {
+ attributes.remove("family_name")
+ }
+ }
+ @Transient
+ var displayName: String?
+ get() = attributes["display_name"]
+ set(value) {
+ if (value != null) {
+ attributes["display_name"] = value
+ } else {
+ attributes.remove("display_name")
+ }
+ }
+ @Transient
+ var email: String?
+ get() = attributes["email"]
+ set(value) {
+ if (value != null) {
+ attributes["email"] = value
+ } else {
+ attributes.remove("email")
+ }
+ }
+ @Transient
+ var pictureUrl: String?
+ get() = attributes["picture_url"]
+ set(value) {
+ if (value != null) {
+ attributes["picture_url"] = value
+ } else {
+ attributes.remove("picture_url")
+ }
+ }
+ @Transient
+ var profileUrl: String?
+ get() = attributes["profile_url"]
+ set(value) {
+ if (value != null) {
+ attributes["profile_url"] = value
+ } else {
+ attributes.remove("profile_url")
+ }
+ }
+}
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
new file mode 100644
index 00000000..cbb2978a
--- /dev/null
+++ b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt
@@ -0,0 +1,208 @@
+/*
+ * 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 kotlinx.serialization.ImplicitReflectionSerializer
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.internal.BooleanSerializer
+import kotlinx.serialization.internal.ByteSerializer
+import kotlinx.serialization.internal.CharSerializer
+import kotlinx.serialization.internal.DoubleSerializer
+import kotlinx.serialization.internal.FloatSerializer
+import kotlinx.serialization.internal.IntSerializer
+import kotlinx.serialization.internal.LongSerializer
+import kotlinx.serialization.internal.ShortSerializer
+import kotlinx.serialization.internal.StringSerializer
+import kotlinx.serialization.list
+import kotlinx.serialization.serializer
+import pl.treksoft.kvision.types.DateSerializer
+import pl.treksoft.kvision.types.toStringF
+import pl.treksoft.kvision.utils.JSON
+import kotlin.js.Date
+import kotlin.reflect.KClass
+
+internal class NotStandardTypeException(type: String) : Exception("Not a standard type: $type!")
+
+internal class NotEnumTypeException : Exception("Not the Enum type!")
+
+/**
+ * Interface for client side agent for JSON-RPC remote calls.
+ */
+interface RemoteAgent {
+
+ /**
+ * @suppress
+ * Internal function
+ */
+ @UseExperimental(ImplicitReflectionSerializer::class)
+ @Suppress("ComplexMethod", "TooGenericExceptionCaught", "NestedBlockDepth")
+ fun trySerialize(kClass: KClass<Any>, value: Any): String {
+ return if (value is List<*>) {
+ if (value.size > 0) {
+ when {
+ value[0] is String ->
+ @Suppress("UNCHECKED_CAST")
+ JSON.plain.stringify(StringSerializer.list as KSerializer<Any>, value)
+ value[0] is Date ->
+ @Suppress("UNCHECKED_CAST")
+ JSON.plain.stringify(DateSerializer.list as KSerializer<Any>, value)
+ value[0] is Int ->
+ @Suppress("UNCHECKED_CAST")
+ JSON.plain.stringify(IntSerializer.list as KSerializer<Any>, value)
+ value[0] is Long ->
+ @Suppress("UNCHECKED_CAST")
+ JSON.plain.stringify(LongSerializer.list as KSerializer<Any>, value)
+ value[0] is Boolean ->
+ @Suppress("UNCHECKED_CAST")
+ JSON.plain.stringify(BooleanSerializer.list as KSerializer<Any>, value)
+ value[0] is Float ->
+ @Suppress("UNCHECKED_CAST")
+ JSON.plain.stringify(FloatSerializer.list as KSerializer<Any>, value)
+ value[0] is Double ->
+ @Suppress("UNCHECKED_CAST")
+ JSON.plain.stringify(DoubleSerializer.list as KSerializer<Any>, value)
+ value[0] is Char ->
+ @Suppress("UNCHECKED_CAST")
+ JSON.plain.stringify(CharSerializer.list as KSerializer<Any>, value)
+ value[0] is Short ->
+ @Suppress("UNCHECKED_CAST")
+ JSON.plain.stringify(ShortSerializer.list as KSerializer<Any>, value)
+ value[0] is Byte ->
+ @Suppress("UNCHECKED_CAST")
+ JSON.plain.stringify(ByteSerializer.list as KSerializer<Any>, value)
+ value[0] is Enum<*> -> "[" + value.joinToString(",") { "\"$it\"" } + "]"
+ else -> try {
+ @Suppress("UNCHECKED_CAST")
+ JSON.plain.stringify(kClass.serializer().list as KSerializer<Any>, value)
+ } catch (e: Throwable) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ JSON.plain.stringify(value[0]!!::class.serializer().list as KSerializer<Any>, value)
+ } catch (e: Throwable) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ JSON.plain.stringify(StringSerializer.list as KSerializer<Any>, value)
+ } catch (e: Throwable) {
+ value.toString()
+ }
+ }
+ }
+ }
+ } else {
+ "[]"
+ }
+ } else {
+ when (value) {
+ is Enum<*> -> "\"$value\""
+ is String -> value
+ is Char -> "\"$value\""
+ is Date -> "\"${value.toStringF()}\""
+ else -> try {
+ @Suppress("UNCHECKED_CAST")
+ JSON.plain.stringify(kClass.serializer(), value)
+ } catch (e: Throwable) {
+ value.toString()
+ }
+ }
+ }
+ }
+
+ /**
+ * @suppress
+ * Internal function
+ */
+ @Suppress("UNCHECKED_CAST", "ComplexMethod")
+ fun <RET> deserialize(value: String, jsType: String): RET {
+ return when (jsType) {
+ "String" -> JSON.plain.parse(StringSerializer, value) as RET
+ "Number" -> JSON.plain.parse(DoubleSerializer, value) as RET
+ "Long" -> JSON.plain.parse(LongSerializer, value) as RET
+ "Boolean" -> JSON.plain.parse(BooleanSerializer, value) as RET
+ "BoxedChar" -> JSON.plain.parse(CharSerializer, value) as RET
+ "Short" -> JSON.plain.parse(ShortSerializer, value) as RET
+ "Date" -> JSON.plain.parse(DateSerializer, value) as RET
+ "Byte" -> JSON.plain.parse(ByteSerializer, value) as RET
+ else -> throw NotStandardTypeException(jsType)
+ }
+ }
+
+ /**
+ * @suppress
+ * Internal function
+ */
+ @Suppress("UNCHECKED_CAST", "ComplexMethod")
+ fun <RET> deserializeList(value: String, jsType: String): List<RET> {
+ return when (jsType) {
+ "String" -> JSON.plain.parse(StringSerializer.list, value) as List<RET>
+ "Number" -> JSON.plain.parse(DoubleSerializer.list, value) as List<RET>
+ "Long" -> JSON.plain.parse(LongSerializer.list, value) as List<RET>
+ "Boolean" -> JSON.plain.parse(BooleanSerializer.list, value) as List<RET>
+ "BoxedChar" -> JSON.plain.parse(CharSerializer.list, value) as List<RET>
+ "Short" -> JSON.plain.parse(ShortSerializer.list, value) as List<RET>
+ "Date" -> JSON.plain.parse(DateSerializer.list, value) as List<RET>
+ "Byte" -> JSON.plain.parse(ByteSerializer.list, value) as List<RET>
+ else -> throw NotStandardTypeException(jsType)
+ }
+ }
+
+ /**
+ * @suppress
+ * Internal function
+ */
+ @Suppress("TooGenericExceptionCaught", "ThrowsCount")
+ fun tryDeserializeEnum(kClass: KClass<Any>, value: String): Any {
+ return try {
+ if (kClass.asDynamic().jClass.`$metadata$`.interfaces[0].name == "Enum") {
+ findEnumValue(kClass, JSON.plain.parse(StringSerializer, value)) ?: throw NotEnumTypeException()
+ } else {
+ throw NotEnumTypeException()
+ }
+ } catch (e: Throwable) {
+ throw NotEnumTypeException()
+ }
+ }
+
+ /**
+ * @suppress
+ * Internal function
+ */
+ @Suppress("TooGenericExceptionCaught", "ThrowsCount")
+ fun tryDeserializeEnumList(kClass: KClass<Any>, value: String): List<Any> {
+ return try {
+ if (kClass.asDynamic().jClass.`$metadata$`.interfaces[0].name == "Enum") {
+ JSON.plain.parse(StringSerializer.list, value).map {
+ findEnumValue(kClass, JSON.plain.parse(StringSerializer, it)) ?: throw NotEnumTypeException()
+ }
+ } else {
+ throw NotEnumTypeException()
+ }
+ } catch (e: Throwable) {
+ throw NotEnumTypeException()
+ }
+ }
+
+ private fun findEnumValue(kClass: KClass<Any>, value: String): Any? {
+ return (kClass.asDynamic().jClass.values() as Array<Any>).find {
+ it.asDynamic().name == value
+ }
+ }
+}
diff --git a/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/Security.kt b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/Security.kt
new file mode 100644
index 00000000..b485e17d
--- /dev/null
+++ b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/Security.kt
@@ -0,0 +1,116 @@
+/*
+ * 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 kotlinx.coroutines.asDeferred
+import kotlinx.serialization.Serializable
+import pl.treksoft.kvision.utils.obj
+
+/**
+ * A security exception.
+ */
+class SecurityException(message: String) : Exception(message)
+
+/**
+ * Username and password credentials.
+ */
+@Serializable
+data class Credentials(val username: String? = null, val password: String? = null)
+
+/**
+ * Pac4j form login dispatcher.
+ */
+class LoginService {
+ val loginAgent = CallAgent()
+
+ /**
+ * Login with Pac4j FormClient.
+ * @param credentials username and password credentials
+ */
+ suspend fun login(credentials: Credentials?): Boolean =
+ if (credentials?.username != null) {
+ loginAgent.remoteCall("callback?client_name=FormClient", obj {
+ this.username = credentials.username
+ this.password = credentials.password
+ }, HttpMethod.POST, "application/x-www-form-urlencoded").then { _: dynamic -> true }.asDeferred().await()
+ } else {
+ throw SecurityException("Credentials cannot be empty")
+ }
+}
+
+/**
+ * Pac4j form login dispatcher.
+ */
+abstract class SecurityMgr {
+
+ private var isLoggedIn = false
+
+ /**
+ * Executes given block of code after successful authentication.
+ * @param block a block of code
+ */
+ @Suppress("NestedBlockDepth", "TooGenericExceptionCaught")
+ suspend fun <T> withAuth(block: suspend () -> T): T {
+ return try {
+ block().also {
+ if (!isLoggedIn) {
+ isLoggedIn = true
+ afterLogin()
+ }
+ }
+ } catch (e: Exception) {
+ if (e is SecurityException || !isLoggedIn) {
+ afterError()
+ isLoggedIn = false
+ while (!isLoggedIn) {
+ try {
+ login()
+ isLoggedIn = true
+ afterLogin()
+ } catch (e: Throwable) {
+ console.log(e)
+ }
+ }
+ block()
+ } else {
+ throw e
+ }
+ }
+ }
+
+ /**
+ * Login user.
+ * @return true if login is successful
+ * @throws SecurityException if login is not successful
+ */
+ abstract suspend fun login(): Boolean
+
+ /**
+ * Method called after successful login.
+ */
+ open suspend fun afterLogin() {}
+
+ /**
+ * Method called after error.
+ */
+ open suspend fun afterError() {}
+}
diff --git a/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/SpringRemoteAgent.kt b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/SpringRemoteAgent.kt
new file mode 100644
index 00000000..fea16b99
--- /dev/null
+++ b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/SpringRemoteAgent.kt
@@ -0,0 +1,370 @@
+/*
+ * 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 kotlinx.coroutines.asDeferred
+import kotlinx.serialization.ImplicitReflectionSerializer
+import kotlinx.serialization.list
+import kotlinx.serialization.serializer
+import pl.treksoft.kvision.utils.JSON
+import kotlin.js.js
+import kotlin.reflect.KClass
+import kotlin.js.JSON as NativeJSON
+
+/**
+ * Client side agent for JSON-RPC remote calls with Spring Boot.
+ */
+@Suppress("LargeClass", "TooManyFunctions")
+@UseExperimental(ImplicitReflectionSerializer::class)
+open class SpringRemoteAgent<T : Any>(val serviceManager: SpringServiceManager<T>) : RemoteAgent {
+
+ val callAgent = CallAgent()
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified RET : Any, T> call(noinline function: suspend T.() -> RET): RET {
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, method = method).then {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ deserialize<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnum(RET::class as KClass<Any>, it) as RET
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer(), it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified RET : Any, T> call(
+ noinline function: suspend T.() -> List<RET>
+ ): List<RET> {
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, method = method).then {
+ try {
+ deserializeList<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnumList(RET::class as KClass<Any>, it) as List<RET>
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer().list, it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR) -> RET, p: PAR
+ ): RET {
+ val data = serialize(p)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data), method).then {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ deserialize<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnum(RET::class as KClass<Any>, it) as RET
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer(), it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR) -> List<RET>, p: PAR
+ ): List<RET> {
+ val data = serialize(p)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data), method).then {
+ try {
+ deserializeList<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnumList(RET::class as KClass<Any>, it) as List<RET>
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer().list, it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR1, reified PAR2, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2) -> RET, p1: PAR1, p2: PAR2
+ ): RET {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2), method).then {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ deserialize<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnum(RET::class as KClass<Any>, it) as RET
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer(), it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR1, reified PAR2, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2) -> List<RET>, p1: PAR1, p2: PAR2
+ ): List<RET> {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2), method).then {
+ try {
+ deserializeList<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnumList(RET::class as KClass<Any>, it) as List<RET>
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer().list, it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR1, reified PAR2, reified PAR3, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2, PAR3) -> RET, p1: PAR1, p2: PAR2, p3: PAR3
+ ): RET {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val data3 = serialize(p3)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2, data3), method).then {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ deserialize<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnum(RET::class as KClass<Any>, it) as RET
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer(), it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR1, reified PAR2, reified PAR3, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2, PAR3) -> List<RET>, p1: PAR1, p2: PAR2, p3: PAR3
+ ): List<RET> {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val data3 = serialize(p3)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2, data3), method).then {
+ try {
+ deserializeList<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnumList(RET::class as KClass<Any>, it) as List<RET>
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer().list, it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4) -> RET, p1: PAR1, p2: PAR2, p3: PAR3, p4: PAR4
+ ): RET {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val data3 = serialize(p3)
+ val data4 = serialize(p4)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2, data3, data4), method).then {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ deserialize<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnum(RET::class as KClass<Any>, it) as RET
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer(), it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ suspend inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4) -> List<RET>,
+ p1: PAR1,
+ p2: PAR2,
+ p3: PAR3,
+ p4: PAR4
+ ): List<RET> {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val data3 = serialize(p3)
+ val data4 = serialize(p4)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2, data3, data4), method).then {
+ try {
+ deserializeList<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnumList(RET::class as KClass<Any>, it) as List<RET>
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer().list, it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ @Suppress("LongParameterList")
+ suspend inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified PAR5,
+ reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4, PAR5) -> RET,
+ p1: PAR1,
+ p2: PAR2,
+ p3: PAR3,
+ p4: PAR4,
+ p5: PAR5
+ ): RET {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val data3 = serialize(p3)
+ val data4 = serialize(p4)
+ val data5 = serialize(p5)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2, data3, data4, data5), method).then {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ deserialize<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnum(RET::class as KClass<Any>, it) as RET
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer(), it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+ /**
+ * Executes defined call to a remote web service.
+ */
+ @Suppress("LongParameterList")
+ suspend inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified PAR5,
+ reified RET : Any, T> call(
+ noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4, PAR5) -> List<RET>,
+ p1: PAR1,
+ p2: PAR2,
+ p3: PAR3,
+ p4: PAR4,
+ p5: PAR5
+ ): List<RET> {
+ val data1 = serialize(p1)
+ val data2 = serialize(p2)
+ val data3 = serialize(p3)
+ val data4 = serialize(p4)
+ val data5 = serialize(p5)
+ val (url, method) =
+ serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!")
+ return callAgent.jsonRpcCall(url, listOf(data1, data2, data3, data4, data5), method).then {
+ try {
+ deserializeList<RET>(it, RET::class.js.name)
+ } catch (t: NotStandardTypeException) {
+ try {
+ @Suppress("UNCHECKED_CAST")
+ tryDeserializeEnumList(RET::class as KClass<Any>, it) as List<RET>
+ } catch (t: NotEnumTypeException) {
+ JSON.nonstrict.parse(RET::class.serializer().list, it)
+ }
+ }
+ }.asDeferred().await()
+ }
+
+
+ /**
+ * @suppress
+ * Internal function
+ */
+ inline fun <reified PAR> serialize(value: PAR): String? {
+ return value?.let {
+ @Suppress("UNCHECKED_CAST")
+ trySerialize((PAR::class as KClass<Any>), it as Any)
+ }
+ }
+
+}
diff --git a/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/SpringServiceManager.kt b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/SpringServiceManager.kt
new file mode 100644
index 00000000..524347d7
--- /dev/null
+++ b/kvision-modules/kvision-remote/src/main/kotlin/pl/treksoft/kvision/remote/SpringServiceManager.kt
@@ -0,0 +1,124 @@
+/*
+ * 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 kotlin.reflect.KClass
+
+/**
+ * Multiplatform service manager for Spring Boot.
+ */
+actual open class SpringServiceManager<T : Any> actual constructor(serviceClass: KClass<T>) : ServiceManager {
+
+ protected val calls: MutableMap<String, Pair<String, RpcHttpMethod>> = mutableMapOf()
+ var counter: Int = 0
+
+ /**
+ * Binds a given route with a function of the receiver.
+ * @param function a function of the receiver
+ * @param route a route
+ * @param method a HTTP method
+ */
+ protected actual inline fun <reified RET> bind(
+ noinline function: suspend T.() -> RET,
+ route: String?, method: RpcHttpMethod
+ ) {
+ val routeDef = route ?: "route${this::class.simpleName}${counter++}"
+ calls[function.toString()] = Pair("/kv/$routeDef", method)
+ }
+
+ /**
+ * Binds a given route with a function of the receiver.
+ * @param function a function of the receiver
+ * @param route a route
+ * @param method a HTTP method
+ */
+ protected actual inline fun <reified PAR, reified RET> bind(
+ noinline function: suspend T.(PAR) -> RET,
+ route: String?, method: RpcHttpMethod
+ ) {
+ val routeDef = route ?: "route${this::class.simpleName}${counter++}"
+ calls[function.toString()] = Pair("/kv/$routeDef", method)
+ }
+
+ /**
+ * Binds a given route with a function of the receiver.
+ * @param function a function of the receiver
+ * @param route a route
+ * @param method a HTTP method
+ */
+ protected actual inline fun <reified PAR1, reified PAR2, reified RET> bind(
+ noinline function: suspend T.(PAR1, PAR2) -> RET,
+ route: String?, method: RpcHttpMethod
+ ) {
+ val routeDef = route ?: "route${this::class.simpleName}${counter++}"
+ calls[function.toString()] = Pair("/kv/$routeDef", method)
+ }
+
+ /**
+ * Binds a given route with a function of the receiver.
+ * @param function a function of the receiver
+ * @param route a route
+ * @param method a HTTP method
+ */
+ protected actual inline fun <reified PAR1, reified PAR2, reified PAR3, reified RET> bind(
+ noinline function: suspend T.(PAR1, PAR2, PAR3) -> RET,
+ route: String?, method: RpcHttpMethod
+ ) {
+ val routeDef = route ?: "route${this::class.simpleName}${counter++}"
+ calls[function.toString()] = Pair("/kv/$routeDef", method)
+ }
+
+ /**
+ * Binds a given route with a function of the receiver.
+ * @param function a function of the receiver
+ * @param route a route
+ * @param method a HTTP method
+ */
+ protected actual inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified RET> bind(
+ noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4) -> RET,
+ route: String?, method: RpcHttpMethod
+ ) {
+ val routeDef = route ?: "route${this::class.simpleName}${counter++}"
+ calls[function.toString()] = Pair("/kv/$routeDef", method)
+ }
+
+ /**
+ * Binds a given route with a function of the receiver.
+ * @param function a function of the receiver
+ * @param route a route
+ * @param method a HTTP method
+ */
+ protected actual inline fun <reified PAR1, reified PAR2, reified PAR3,
+ reified PAR4, reified PAR5, reified RET> bind(
+ noinline function: suspend T.(PAR1, PAR2, PAR3, PAR4, PAR5) -> RET,
+ route: String?,
+ method: RpcHttpMethod
+ ) {
+ val routeDef = route ?: "route${this::class.simpleName}${counter++}"
+ calls[function.toString()] = Pair("/kv/$routeDef", method)
+ }
+
+ /**
+ * Returns the map of defined paths.
+ */
+ override fun getCalls(): Map<String, Pair<String, RpcHttpMethod>> = calls
+}
diff --git a/kvision-modules/kvision-remote/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt b/kvision-modules/kvision-remote/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt
new file mode 100644
index 00000000..1da1fe1a
--- /dev/null
+++ b/kvision-modules/kvision-remote/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt
@@ -0,0 +1,99 @@
+/*
+ * 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 test.pl.treksoft.kvision
+
+import org.w3c.dom.Element
+import pl.treksoft.jquery.jQuery
+import pl.treksoft.kvision.core.Widget
+import pl.treksoft.kvision.panel.Root
+import kotlin.browser.document
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+interface TestSpec {
+ fun beforeTest()
+
+ fun afterTest()
+
+ fun run(code: () -> Unit) {
+ beforeTest()
+ code()
+ afterTest()
+ }
+}
+
+interface SimpleSpec : TestSpec {
+
+ override fun beforeTest() {
+ }
+
+ override fun afterTest() {
+ }
+
+}
+
+interface DomSpec : TestSpec {
+
+ override fun beforeTest() {
+ val fixture = "<div style=\"display: none\" id=\"pretest\">" +
+ "<div id=\"test\"></div></div>"
+ document.body?.insertAdjacentHTML("afterbegin", fixture)
+ }
+
+ override fun afterTest() {
+ val div = document.getElementById("pretest")
+ div?.let { jQuery(it).remove() }
+ jQuery(`object` = ".modal-backdrop").remove()
+ }
+
+ fun assertEqualsHtml(expected: String?, actual: String?, message: String?) {
+ if (expected != null && actual != null) {
+ val exp = jQuery(html = expected)
+ val act = jQuery(html = actual)
+ val result = exp[0]?.isEqualNode(act[0])
+ if (result == true) {
+ assertTrue(result == true, message)
+ } else {
+ assertEquals(expected, actual, message)
+ }
+ } else {
+ assertEquals(expected, actual, message)
+ }
+ }
+}
+
+interface WSpec : DomSpec {
+
+ fun runW(code: (widget: Widget, element: Element?) -> Unit) {
+ run {
+ val root = Root("test", true)
+ val widget = Widget()
+ widget.id = "test_id"
+ root.add(widget)
+ val element = document.getElementById("test_id")
+ code(widget, element)
+ }
+ }
+
+}
+
+external fun require(name: String): dynamic
diff --git a/kvision-modules/kvision-remote/src/test/kotlin/test/pl/treksoft/kvision/form/upload/UploadInputSpec.kt b/kvision-modules/kvision-remote/src/test/kotlin/test/pl/treksoft/kvision/form/upload/UploadInputSpec.kt
new file mode 100644
index 00000000..626b70e4
--- /dev/null
+++ b/kvision-modules/kvision-remote/src/test/kotlin/test/pl/treksoft/kvision/form/upload/UploadInputSpec.kt
@@ -0,0 +1,57 @@
+/*
+ * 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 test.pl.treksoft.kvision.form.upload
+
+import pl.treksoft.jquery.jQuery
+import pl.treksoft.kvision.form.upload.UploadInput
+import pl.treksoft.kvision.panel.Root
+import test.pl.treksoft.kvision.DomSpec
+import kotlin.browser.document
+import kotlin.test.Test
+
+class UploadInputSpec : DomSpec {
+
+ @Test
+ fun render() {
+ run {
+ val root = Root("test", true)
+ val upi = UploadInput(multiple = true).apply {
+ id = "idti"
+ }
+ root.add(upi)
+ val content = document.getElementById("test")?.let { jQuery(it).find("input.form-control")[0]?.outerHTML }
+ assertEqualsHtml(
+ "<input class=\"form-control\" id=\"idti\" type=\"file\" multiple=\"true\">",
+ content,
+ "Should render correct file input control for multiple files"
+ )
+ upi.multiple = false
+ val content2 = document.getElementById("test")?.let { jQuery(it).find("input.form-control")[0]?.outerHTML }
+ assertEqualsHtml(
+ "<input class=\"form-control\" id=\"idti\" type=\"file\">",
+ content2,
+ "Should render correct file input control for single file"
+ )
+ }
+ }
+
+}
diff --git a/kvision-modules/kvision-remote/src/test/kotlin/test/pl/treksoft/kvision/form/upload/UploadSpec.kt b/kvision-modules/kvision-remote/src/test/kotlin/test/pl/treksoft/kvision/form/upload/UploadSpec.kt
new file mode 100644
index 00000000..bea4ddee
--- /dev/null
+++ b/kvision-modules/kvision-remote/src/test/kotlin/test/pl/treksoft/kvision/form/upload/UploadSpec.kt
@@ -0,0 +1,56 @@
+/*
+ * 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 test.pl.treksoft.kvision.form.upload
+
+import pl.treksoft.jquery.jQuery
+import pl.treksoft.kvision.form.upload.Upload
+import pl.treksoft.kvision.panel.Root
+import test.pl.treksoft.kvision.DomSpec
+import kotlin.browser.document
+import kotlin.test.Test
+
+class UploadSpec : DomSpec {
+
+ @Test
+ fun render() {
+ run {
+ val root = Root("test", true)
+ val upi = Upload(multiple = true)
+ val id = upi.input.id
+ root.add(upi)
+ val content = document.getElementById("test")?.let { jQuery(it).find("input.form-control")[0]?.outerHTML }
+ assertEqualsHtml(
+ "<input class=\"form-control\" id=\"$id\" type=\"file\" multiple=\"true\">",
+ content,
+ "Should render correct file input control for multiple files"
+ )
+ upi.multiple = false
+ val content2 = document.getElementById("test")?.let { jQuery(it).find("input.form-control")[0]?.outerHTML }
+ assertEqualsHtml(
+ "<input class=\"form-control\" id=\"$id\" type=\"file\">",
+ content2,
+ "Should render correct file input control for single file"
+ )
+ }
+ }
+
+}