Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
package io.kform.collections
import io.kform.AbsolutePath
import io.kform.AbsolutePathFragment
import io.kform.Path
import io.kform.toAbsolutePath
/** Entry in a path trie holding values of type [T]. */
public data class PathTrieEntry(
override val path: AbsolutePath,
override val value: T,
override val id: PathMultimapEntryId
) : PathMultimapEntry
/**
* Implementation of a mutable path multimap as a trie. Held values are of type [T].
*
* Each node of the trie represents a path fragment. As such, a path can be represented by
* navigating through the trie's nodes. Each node contains the values associated with the path they
* represent.
*
* A trie is a good structure to implement a path multimap due to the intended semantics of the
* [get(path)][get] and [remove(path)][removeEntry] functions: the `get` function should return all
* values matching a given path, whilst the `remove` function removes values contained by a given
* path (following the [matching][AbsolutePath.matches] and [containment][AbsolutePath.contains]
* semantics of paths).
*
* As an example, imagine the following operations on the trie (with simplified notation for the
* paths):
* ```kotlin
* trie["/"] = 0
* trie["/path/x"] = 1
* trie["/path/y"] = 2
* trie["/path/x/z"] = 3
* trie["/∗/path"] = 4
* trie["/∗/path"] = 5
* ```
*
* The resulting structure would look similar to the following, where the right side represents the
* values associated with each trie node:
* ```
* / -> [0]
* ├─ path -> []
* │ ├─ x -> [1]
* │ │ └─ z -> [3]
* │ └─ y -> [2]
* └─ ∗ -> []
* └─ path -> [4, 5]
* ```
*
* Getting values associated with the path `"/∗/∗"` would return a sequence with values `[1, 2, 4,
* 5]`.
*
* @constructor Creates a new path trie given an `initialCapacity` (defaults to 100).
*/
public class PathTrie(initialCapacity: Int = 100) : MutablePathMultimap {
/**
* Class representing a node of the trie representing the fragment [fragment] and whose parent
* node is [parent].
*/
private class PathTrieNode(
val parent: PathTrieNode? = null,
val fragment: AbsolutePathFragment? = null
) {
/** Child nodes. */
val childNodes: MutableMap> =
HashMap(CHILD_NODES_MAP_INITIAL_CAPACITY)
/** Entries of this node. */
val entries: MutableMap> =
HashMap(ENTRIES_MAP_INITIAL_CAPACITY)
/** Creates and returns a new child node for the fragment [fragment]. */
fun newChildNode(fragment: AbsolutePathFragment): PathTrieNode {
val newNode = PathTrieNode(this, fragment)
childNodes[fragment] = newNode
return newNode
}
/** Clears this node. */
fun clear() {
childNodes.clear()
entries.clear()
}
/** Cleans up this node by removing itself from its parent when it is empty. */
fun cleanUp() {
if (parent != null && childNodes.isEmpty() && entries.isEmpty()) {
parent.childNodes.remove(fragment!!)
parent.cleanUp()
}
}
private companion object {
// Initial capacities for the node maps
const val CHILD_NODES_MAP_INITIAL_CAPACITY = 10
// Assumes that typically only one value is associated with each
const val ENTRIES_MAP_INITIAL_CAPACITY = 1
}
}
/** Root node of the trie. */
private val rootNode: PathTrieNode = PathTrieNode()
/** Nodes containing each key, for fast access via key. */
private val nodesById: MutableMap> =
HashMap(initialCapacity)
/** Next entry identifier to use. */
private var entryId: PathMultimapEntryId = 0
override val size: Int
get() = nodesById.size
/**
* Formats the trie as a mapping of paths to the list of values associated with them.
*
* Example: `"{/path1=[val1, val2], /path2=[val3, val4]}"`.
*/
override fun toString(): String = toMap().toString()
override fun containsPath(path: Path): Boolean =
path.toAbsolutePath().let { absolutePath -> entriesImpl(absolutePath).any() }
override fun containsEntry(entryId: PathMultimapEntryId): Boolean =
nodesById.containsKey(entryId)
override fun containsValue(value: T): Boolean =
entriesImpl(AbsolutePath.MATCH_ALL).any { (_, value2) -> value == value2 }
override fun get(path: Path): Sequence =
path.toAbsolutePath().let { absolutePath ->
entriesImpl(absolutePath).map { (_, value) -> value }
}
override fun getEntry(entryId: PathMultimapEntryId): PathTrieEntry? {
val node = nodesById[entryId] ?: return null
val pair = node.entries[entryId]!!
return PathTrieEntry(pair.first, pair.second, entryId)
}
override fun put(path: Path, value: T): PathMultimapEntryId =
path.toAbsolutePath().let { absolutePath ->
val newId = entryId++
var curNode = this.rootNode
for (fragment in absolutePath) {
var nextNode = curNode.childNodes[fragment]
if (nextNode == null) {
nextNode = curNode.newChildNode(fragment)
}
curNode = nextNode
}
nodesById[newId] = curNode
curNode.entries[newId] = absolutePath to value
return newId
}
override fun remove(path: Path): List> =
path.toAbsolutePath().let { absolutePath ->
val removedEntries = mutableListOf>()
val cache = mutableSetOf>()
removeImpl(absolutePath, removedEntries, cache)
// Clean up all nodes from which we may have removed an entry since we cannot clean them
// up
// whilst iterating on them in [removeImpl]
for (node in cache) {
node.cleanUp()
}
return removedEntries
}
override fun removeEntry(entryId: PathMultimapEntryId): PathTrieEntry? {
val node = nodesById[entryId] ?: return null
val pair = node.entries.remove(entryId)!!
node.cleanUp()
nodesById.remove(entryId)
return PathTrieEntry(pair.first, pair.second, entryId)
}
override fun clear() {
rootNode.clear()
nodesById.clear()
}
override fun entries(path: Path): Sequence> =
path.toAbsolutePath().let { absolutePath -> entriesImpl(absolutePath) }
/**
* Returns a sequence over all entries matching [path] (recursive implementation).
*
* @param cache Set containing the nodes whose entries we've already yielded. The algorithm can
* end up analysing the same node more than once, hence the need to keep track of the nodes
* with entries already yielded.
* @param i Index of the fragment of the path being analysed.
* @param node Current node being analysed.
*/
private fun entriesImpl(
path: AbsolutePath,
cache: MutableSet> = mutableSetOf(),
i: Int = 0,
node: PathTrieNode = rootNode
): Sequence> = sequence {
// Base case (whole path matched)
if (i == path.size) {
if (!cache.contains(node)) {
cache += node
for ((id, pair) in node.entries) {
yield(PathTrieEntry(pair.first, pair.second, id))
}
}
} else {
when (val fragment = path[i]) {
AbsolutePathFragment.RecursiveWildcard -> {
// Match next fragment against same node
yieldAll(entriesImpl(path, cache, i + 1, node))
// Match same fragment against all next nodes except the recursive wildcard one
// (which is matched against at the end of the method)
for ((nextFragment, nextNode) in node.childNodes.entries) {
if (nextFragment != AbsolutePathFragment.RecursiveWildcard) {
yieldAll(entriesImpl(path, cache, i, nextNode))
}
}
}
AbsolutePathFragment.Wildcard -> {
// Match next fragment against all next nodes except the recursive wildcard one
// (which is matched against at the end of the method)
for ((nextFragment, nextNode) in node.childNodes.entries) {
if (nextFragment !== AbsolutePathFragment.RecursiveWildcard) {
yieldAll(entriesImpl(path, cache, i + 1, nextNode))
}
}
}
else -> {
// Match next fragment against matching node
val nextNode = node.childNodes[fragment]
if (nextNode != null) {
yieldAll(entriesImpl(path, cache, i + 1, nextNode))
}
// Match next fragment against non-recursive wildcard node
val wildcardNode = node.childNodes[AbsolutePathFragment.Wildcard]
if (wildcardNode != null) {
yieldAll(entriesImpl(path, cache, i + 1, wildcardNode))
}
}
}
}
// Match same fragment and all next fragments against the recursive wildcard node
val recWildcardNode = node.childNodes[AbsolutePathFragment.RecursiveWildcard]
if (recWildcardNode != null) {
for (j in i..path.size) {
yieldAll(entriesImpl(path, cache, j, recWildcardNode))
}
}
}
/**
* Removes all entries with a path contained by [path] and adds them to a list of removed
* entries provided in [removedEntries].
*
* Because we cannot clean up nodes whilst iterating on them, this function's caller should
* clean up all nodes in the [cache] after calling this function (and as such it has to provide
* the cache itself as well).
*
* @param cache Set containing the nodes whose entries we've already removed. The algorithm can
* end up analysing the same node more than once, hence the need to keep track of the nodes
* with entries already removed.
* @param i Index of the fragment of the path being analysed.
* @param node Current node being analysed.
*/
private fun removeImpl(
path: AbsolutePath,
removedEntries: MutableList>,
cache: MutableSet>,
i: Int = 0,
node: PathTrieNode = rootNode
) {
// Base case (whole path matched)
if (i == path.size) {
if (!cache.contains(node)) {
cache += node
if (node.entries.isNotEmpty()) {
for ((id, pair) in node.entries) {
removedEntries += PathTrieEntry(pair.first, pair.second, id)
nodesById.remove(id)
}
node.entries.clear()
}
}
} else {
when (val fragment = path[i]) {
AbsolutePathFragment.RecursiveWildcard -> {
// Match next fragment against same node
removeImpl(path, removedEntries, cache, i + 1, node)
// Match same fragment against all next nodes
for (nextNode in node.childNodes.values) {
removeImpl(path, removedEntries, cache, i, nextNode)
}
}
AbsolutePathFragment.Wildcard -> {
// Match next fragment against all next nodes except the recursive wildcard one
for ((nextFragment, nextNode) in node.childNodes.entries) {
if (nextFragment != AbsolutePathFragment.RecursiveWildcard) {
removeImpl(path, removedEntries, cache, i + 1, nextNode)
}
}
}
else -> {
// Match next fragment against matching node
val nextNode = node.childNodes[fragment]
if (nextNode != null) {
removeImpl(path, removedEntries, cache, i + 1, nextNode)
}
}
}
}
}
}