Skip to content

Instantly share code, notes, and snippets.

@resengupta
Last active November 13, 2019 14:20
Show Gist options
  • Save resengupta/c22e0096b171d99c95000605f8b782d1 to your computer and use it in GitHub Desktop.
Save resengupta/c22e0096b171d99c95000605f8b782d1 to your computer and use it in GitHub Desktop.
Example on how to use dynamic proxy design pattern. Example uses Duktape client to create a generic client provider implementation, where we can create our own interface class by using - getImplementation(clazz: Class<T>), which return Interface Instance where the methods can return Object instead of String, as Duktape default return type is Str…
@kotlin.annotation.Target(AnnotationTarget.VALUE_PARAMETER)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class Input(val value: String)
interface JsDemoEndpoint {
fun getAggregatedData(dataList: List<Data>?): DataView?
fun getAggregatedDataByMultipleInput(@Input("first") dataList: List<Data>?,
@Input("second") data: Data?): DataView?
}
import android.content.Context
import android.util.Log
import com.google.gson.Gson
import com.myapps.jsdemo.BuildConfig
import com.myapps.jsdemo.extensions.log
import com.squareup.duktape.Duktape
import java.lang.reflect.Proxy
import java.lang.reflect.Type
class JsEngineProvider(private val context: Context,
private val gson: Gson) {
// Synchronized
private val duktape by lazy { Duktape.create() }
private lateinit var jsBinder: JsBinder
fun evaluate(jsFile: String, jsScope: String) {
val script = context.assets.open(jsFile).bufferedReader().use { it.readText() }
duktape.evaluate(script)
jsBinder = duktape.get(jsScope, JsBinder::class.java)
}
fun close() {
duktape.close()
}
/**
* This method get the endpoint, and does the default implementation where it needs to call
* the correct method with appropriate input. As of now we send only 1 input via JS engine.
*
* If return type is list, then extra [Type] parameter is required in the end of all parameters.
*/
fun <T> getImplementation(clazz: Class<T>): T = Proxy.newProxyInstance(
clazz.classLoader,
arrayOf<Class<*>>(clazz)
) { _, method, args ->
// args can be null when no param is passed
val type: Type? = args?.find { it is Type } as? Type // Type should always be the last parameter
val parameterAnnotationsArray: Array<Array<Annotation>> = method.parameterAnnotations
val input: Any? = if (parameterAnnotationsArray.isEmpty() || parameterAnnotationsArray.all { it.isEmpty() }) {
args?.firstOrNull()
} else {
parameterAnnotationsArray.mapIndexedNotNull { index, arrayOfAnnotations ->
val key = (arrayOfAnnotations.firstOrNull() as? Input)?.value
key?.let { Pair(key, args?.getOrNull(index)) }
}.associate { it.first to it.second }
}
if (type != null) {
get(method.name, input, type)
} else {
get(method.name, input, method.returnType)
}
} as T
private fun <T> get(functionName: String, input: Any?, returnType: Class<T>): T? {
return try {
if (BuildConfig.DEBUG) log("TAG", "JS::: $functionName --> " + gson.toJson(input))
val results: String? = jsBinder.androidWrapper(functionName, gson.toJson(input))
if (BuildConfig.DEBUG) log("TAG", "JS::: $functionName <-- $results")
gson.fromJson(results, returnType)
} catch (e: Exception) {
if (BuildConfig.DEBUG) Log.e("TAG", "JS::: $functionName --> ", e)
null
}
}
private fun <T> get(functionName: String, input: Any?, typeToken: Type): T? {
return try {
if (BuildConfig.DEBUG) log("TAG", "JS::: $functionName --> " + gson.toJson(input))
val results: String? = jsBinder.androidWrapper(functionName, gson.toJson(input))
if (BuildConfig.DEBUG) log("TAG", "JS::: $functionName <-- $results")
gson.fromJson(results, typeToken)
} catch (e: Exception) {
if (BuildConfig.DEBUG) Log.e("TAG", "JS::: $functionName --> ", e)
null
}
}
interface JsBinder {
fun androidWrapper(functionName: String, input: String): String
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment