main.recursion.ChildFinder.kt Maven / Gradle / Ivy
package de.peekandpoke.ultra.common.recursion
import de.peekandpoke.ultra.common.prepend
import de.peekandpoke.ultra.common.recursion.ChildFinder.Companion.find
import kotlin.reflect.KClass
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
/**
* Helper class that descends into an object to find all children and grand-children of the given type
*
* Use the [find] methods to initiate the search.
*
* TODO: tests
*/
class ChildFinder private constructor(
private val searchedClass: KClass,
private val inTarget: T,
private val predicate: (C) -> Boolean
) {
/** A set of nodes that we already visited. Used to break cyclic graphs */
private val visited = mutableSetOf()
/** A set of nodes that match the filter criteria */
private val found = mutableSetOf>()
data class Found(val item: C, val path: String, val parents: List) {
/**
* Get's the parent at the given [idx] or null
*/
fun parent(idx: Int): Any? {
return parents.getOrNull(idx)
}
}
companion object {
/**
* Finds all children of [target] that are of exactly of the class [cls]
*/
fun find(cls: KClass, target: T) = find(cls, target) { true }
/**
* Finds all children of [target] that are of exactly of the class [cls] and that match the [predicate]
*/
fun find(cls: KClass, target: T, predicate: (C) -> Boolean) =
ChildFinder(cls, target, predicate).run()
}
fun run(): List> {
visited.clear()
found.clear()
visit(inTarget, "root", emptyList())
return found.toList()
}
private fun visit(target: Any?, path: String, parents: List) {
// TODO: We need special tests for this, because data class collide with their own hashcode.
// Still if we have multiple data classes with same hashCode the identityHashCode should be different.
val idHashCode = System.identityHashCode(target)
if (target == null || visited.contains(idHashCode)) {
return
}
visited.add(idHashCode)
@Suppress("UNCHECKED_CAST")
if (target::class == searchedClass && predicate(target as C)) {
found.add(
Found(
item = target,
path = path,
parents = parents,
)
)
return
}
when (target) {
is List<*> -> visitCollection(target, path, parents)
is Map<*, *> -> visitCollection(target.values, path, parents)
else -> visitObject(target, path, parents)
}
}
private fun visitCollection(collection: Collection<*>, path: String, parents: List) {
val parentsWithCollection = parents.prepend(collection)
collection.forEachIndexed { idx, it -> visit(it, "$path.$idx", parentsWithCollection) }
}
private fun visitObject(target: Any, path: String, parents: List) {
val parentsWithTarget = parents.prepend(target)
val reflect = target::class
// Blacklist internal java and kotlin classes
val fqn = reflect.qualifiedName
if (fqn != null && (fqn.startsWith("java.") || fqn.startsWith("kotlin."))) {
return
}
reflect.memberProperties
.forEach {
visit(
target = it.javaField?.apply { it.isAccessible = true }?.get(target),
path = "$path.${it.name}",
parentsWithTarget
)
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy