Skip to content

Commit

Permalink
refactor: extracted reflection based function calling to be reusable
Browse files Browse the repository at this point in the history
  • Loading branch information
AlmasB committed Feb 10, 2024
1 parent 1bd2edc commit 17236fb
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 51 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* FXGL - JavaFX Game Library. The MIT License (MIT).
* Copyright (c) AlmasB ([email protected]).
* See LICENSE for details.
*/

package com.almasb.fxgl.core.reflect

import java.lang.RuntimeException
import java.lang.reflect.Method
import java.util.function.BiFunction

/**
* Allows calling methods of any object using its String name
* and String arguments.
*
* @author Almas Baim (https://github.com/AlmasB)
*/
class ReflectionFunctionCaller {

private val functions = hashMapOf<FunctionSignature, ReflectionFunction>()

/**
* Provides conversions from [String] to other types.
*/
private val stringToObject = hashMapOf<Class<*>, (String) -> Any>()

var defaultFunctionHandler: BiFunction<String, List<String>, Any> = BiFunction { name, args ->
throw RuntimeException("No function handler for $name with $args")
}

val methods: List<Method>
get() = functions.values.map { it.method }

init {
// register common types
stringToObject[String::class.java] = { it }
stringToObject[Int::class.java] = { it.toInt() }
stringToObject[Double::class.java] = { it.toDouble() }
stringToObject[Boolean::class.java] = {
when (it) {
"true" -> true
"false" -> false
else -> throw RuntimeException("Cannot convert $it to Boolean")
}
}
stringToObject[List::class.java] = { it.split(",") }
}

fun <T : Any> addStringToObjectConverter(type: Class<T>, converter: (String) -> T) {
stringToObject[type] = converter
}

fun removeStringToObjectConverter(type: Class<*>) {
stringToObject.remove(type)
}

/**
* Add all methods (incl. private) of [targetObject] to be invokable
* by this reflection function caller.
*/
fun addFunctionCallTarget(targetObject: Any) {
targetObject.javaClass.declaredMethods.forEach {
val signature = FunctionSignature(it.name, it.parameterCount)
val method = ReflectionFunction(targetObject, it)
it.isAccessible = true

functions[signature] = method
}
}

/**
* Remove all methods of a previously added [targetObject].
*/
fun removeFunctionCallTarget(targetObject: Any) {
functions.filterValues { it.functionCallTarget === targetObject }
.forEach { functions.remove(it.key) }
}

/**
* @return true if [functionName] with [paramCount] number of parameters exists
*/
fun exists(functionName: String, paramCount: Int): Boolean {
return functions.containsKey(FunctionSignature(functionName, paramCount))
}

fun call(functionName: String, args: List<String>): Any {
return call(functionName, args.toTypedArray())
}

fun call(functionName: String, args: Array<String>): Any {
val function = functions[FunctionSignature(functionName, args.size)]

if (function != null) {
val argsAsObjects = function.method.parameterTypes.mapIndexed { index, type ->
val converter = stringToObject[type] ?: throw java.lang.RuntimeException("No converter found from String to $type")
converter.invoke(args[index])
}

// void returns null, but Any is expected, so we return 0 in such cases
return function.method.invoke(function.functionCallTarget, *argsAsObjects.toTypedArray()) ?: 0
}

return defaultFunctionHandler.apply(functionName, args.toList())
}

private data class FunctionSignature(val name: String, val paramCount: Int)

/**
* Stores the object [functionCallTarget] and the function [method] that can be invoked on the object.
*/
private data class ReflectionFunction(val functionCallTarget: Any, val method: Method)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
package com.almasb.fxgl.cutscene.dialogue

import com.almasb.fxgl.core.collection.PropertyMap
import com.almasb.fxgl.core.reflect.ReflectionFunctionCaller
import com.almasb.fxgl.logging.Logger
import java.lang.reflect.Method
import java.util.function.BiFunction

/**
*
Expand Down Expand Up @@ -275,67 +276,35 @@ abstract class FunctionCallHandler : FunctionCallDelegate {

private val log = Logger.get(javaClass)

private val methods = hashMapOf<MethodSignature, InvokableMethod>()

/**
* Provides conversions from [String] to other types.
*/
val stringToObject = hashMapOf<Class<*>, (String) -> Any>()
val rfc = ReflectionFunctionCaller()

init {
// register common types
stringToObject[String::class.java] = { it }
stringToObject[Int::class.java] = { it.toInt() }
stringToObject[Double::class.java] = { it.toDouble() }
stringToObject[Boolean::class.java] = {
when (it) {
"true" -> true
"false" -> false
else -> throw java.lang.RuntimeException("Cannot convert $it to Boolean")
}
}

// add self as a delegate so that all methods of the implementing class
// are automatically added into [methods]
addFunctionCallDelegate(this)
rfc.addFunctionCallTarget(this)
rfc.defaultFunctionHandler = BiFunction { name, args ->
handle(name, args.toTypedArray())
}
}

fun addFunctionCallDelegate(obj: FunctionCallDelegate) {
obj.javaClass.declaredMethods.forEach {
val signature = MethodSignature(it.name, it.parameterCount)
val method = InvokableMethod(obj, it)
rfc.addFunctionCallTarget(obj)

methods[signature] = method

log.debug("Added cmd: $method ($signature)")
rfc.methods.forEach {
log.debug("Added cmd: $it")
}
}

fun removeFunctionCallDelegate(obj: FunctionCallDelegate) {
methods.filterValues { it.delegate === obj }
.forEach { methods.remove(it.key) }
rfc.removeFunctionCallTarget(obj)
}

fun exists(functionName: String, paramCount: Int): Boolean {
return methods.containsKey(MethodSignature(functionName, paramCount))
return rfc.exists(functionName, paramCount)
}

fun call(functionName: String, args: Array<String>): Any {
val method = methods[MethodSignature(functionName, args.size)]

if (method != null) {
val argsAsObjects = method.function.parameterTypes.mapIndexed { index, type ->
val converter = stringToObject[type] ?: throw java.lang.RuntimeException("No converter found from String to $type")
converter.invoke(args[index])
}

// void returns null, but Any is expected, so we return 0 in such cases
return method.function.invoke(method.delegate, *argsAsObjects.toTypedArray()) ?: 0
}

log.warning("Unrecognized function: $functionName with ${args.size} arguments. Calling default implementation")

return handle(functionName, args)
return rfc.call(functionName, args)
}

/**
Expand All @@ -346,11 +315,4 @@ abstract class FunctionCallHandler : FunctionCallDelegate {
log.warning("$functionName ${args.toList()}")
return 0
}

private data class MethodSignature(val name: String, val paramCount: Int)

/**
* Stores the object [delegate] and the function [function] that can be invoked on the object.
*/
private data class InvokableMethod(val delegate: FunctionCallDelegate, val function: Method)
}

0 comments on commit 17236fb

Please sign in to comment.