KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/util/SBPattern.kt

279 lines
7.1 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ru.dbotthepony.kstarbound.util
import com.github.benmanes.caffeine.cache.Interner
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.Starbound
/**
* Шаблонизировання строка в стиле Starbound'а
*
* Представляет из себя строки вида:
* * `my.thing`
* * `frame_<FrameNumber>`
* * `<frame><effectDirectives>`
*/
class SBPattern private constructor(
val raw: String,
private val params: Array<String?>,
val pieces: ImmutableList<Piece>,
val namesSet: ImmutableSet<String>,
val namesList: ImmutableList<String>,
) {
fun getParam(name: String): String? {
val index = namesList.indexOf(name)
if (index == -1) return null
return params[index]
}
val isPlainString get() = namesSet.isEmpty()
val value by lazy { resolve { null } }
override fun toString(): String {
return "SBPattern[$raw, ${params.withIndex().joinToString("; ") { "${namesList[it.index]}=${it.value}" }}]"
}
override fun equals(other: Any?): Boolean {
return other === this || other is SBPattern && other.raw == raw && other.namesSet == namesSet && other.pieces == pieces && other.params.contentEquals(params)
}
@Volatile
private var calculatedHash = false
@Volatile
private var hash = 0
override fun hashCode(): Int {
if (!calculatedHash) {
hash = raw.hashCode().xor(params.contentHashCode()).rotateLeft(12).and(pieces.hashCode()).rotateRight(8).xor(namesSet.hashCode())
calculatedHash = true
}
return hash
}
/**
* Different from regular resolve that this always returns non-null string,
* even if we didn't replace all tags (not replaced tags appear as `<tags>` or as [defaultValue],
* depending on [replaceWithDefault])
*/
fun resolveOrSkip(values: (String) -> String?, replaceWithDefault: Boolean = false, defaultValue: String = ""): String {
if (namesSet.isEmpty()) {
return raw
} else if (pieces.size == 1) {
val resolve = pieces[0].resolve(values, this::getParam)
?: if (replaceWithDefault) {
return defaultValue
} else {
return raw
}
return resolve
}
val buffer = ArrayList<String>(pieces.size)
for (piece in pieces) {
var resolve = piece.resolve(values, this::getParam)
if (resolve == null) {
if (replaceWithDefault) {
resolve = defaultValue
} else {
resolve = "<${piece.name!!}>"
}
}
buffer.add(resolve)
}
var count = 0
for (piece in buffer) count += piece.length
val builder = StringBuilder(count)
for (piece in buffer) builder.append(piece)
return String(builder)
}
fun resolve(values: (String) -> String?): String? {
if (namesSet.isEmpty()) {
return raw
} else if (pieces.size == 1) {
return pieces[0].resolve(values, this::getParam)
}
val buffer = ArrayList<String>(pieces.size)
for (piece in pieces) {
buffer.add(piece.resolve(values, this::getParam) ?: return null)
}
var count = 0
for (piece in buffer) count += piece.length
val builder = StringBuilder(count)
for (piece in buffer) builder.append(piece)
return String(builder)
}
fun resolve(values: Map<String, String>): String? {
return resolve(values::get)
}
fun with(params: Map<String, String>): SBPattern {
return with(params::get)
}
fun with(params: (String) -> String?): SBPattern {
when (namesList.size) {
0 -> return this
1 -> {
val get = params.invoke(namesList[0])
if (get == this.params[0]) {
return this
}
return SBPattern(raw, arrayOf(get), pieces, namesSet, namesList)
}
2 -> {
val get0 = params.invoke(namesList[0])
val get1 = params.invoke(namesList[1])
if (get0 == this.params[0] && get1 == this.params[1]) {
return this
}
return SBPattern(raw, arrayOf(get0, get1), pieces, namesSet, namesList)
}
3 -> {
val get0 = params.invoke(namesList[0])
val get1 = params.invoke(namesList[1])
val get2 = params.invoke(namesList[2])
if (get0 == this.params[0] && get1 == this.params[1] && get2 == this.params[2]) {
return this
}
return SBPattern(raw, arrayOf(get0, get1, get2), pieces, namesSet, namesList)
}
else -> {
val newArray = this.params.copyOf()
var any = false
for ((i, name) in namesList.withIndex()) {
val value = this.params[i]
val get = params.invoke(name)
if (value != get) {
newArray[i] = value
any = true
}
}
if (!any)
return this
return SBPattern(raw, newArray, pieces, namesSet, namesList)
}
}
}
data class Piece(val name: String? = null, val contents: String? = null) {
init {
check(name != null || contents != null) { "Both name and contents are null" }
check(!(name != null && contents != null)) { "Both name and contents are not null" }
}
inline fun resolve(map0: (String) -> String?, map1: (String) -> String?): String? {
return contents ?: map0.invoke(name!!) ?: map1.invoke(name!!)
}
}
companion object : TypeAdapterFactory {
private val interner: Interner<SBPattern> = Starbound.interner(5)
@JvmField
val EMPTY = raw("")
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType == SBPattern::class.java) {
return object : TypeAdapter<SBPattern>() {
private val strings = gson.getAdapter(String::class.java)
override fun write(out: JsonWriter, value: SBPattern?) {
strings.write(out, value?.raw)
}
override fun read(`in`: JsonReader): SBPattern? {
return of(strings.read(`in`) ?: return null)
}
} as TypeAdapter<T>
}
return null
}
@JvmStatic
fun of(raw: String): SBPattern {
if (raw == "")
return EMPTY
val pieces = ImmutableList.Builder<Piece>()
var i = 0
while (i < raw.length) {
val open = raw.indexOf('<', startIndex = i)
if (open == -1) {
if (i == 0)
pieces.add(Piece(contents = raw))
else
pieces.add(Piece(contents = raw.substring(i)))
break
} else {
if (open != i) {
pieces.add(Piece(contents = raw.substring(i, open)))
}
val closing = raw.indexOf('>', startIndex = open + 1)
if (closing == -1) {
throw IllegalArgumentException("Malformed pattern string: $raw")
}
pieces.add(Piece(name = Starbound.STRINGS.intern(raw.substring(open + 1, closing))))
i = closing + 1
}
}
val built = pieces.build()
val names = built.stream().map { it.name }.filter { it != null }.collect(ImmutableSet.toImmutableSet())
val result = SBPattern(
raw,
pieces = built,
params = arrayOfNulls(names.size),
namesSet = names as ImmutableSet<String>,
namesList = ImmutableList.copyOf(names)
)
return interner.intern(result)
}
private val emptyParams = arrayOfNulls<String>(0)
@JvmStatic
fun raw(raw: String): SBPattern {
if (raw == "")
return EMPTY
return SBPattern(raw, emptyParams, ImmutableList.of(), ImmutableSet.of(), ImmutableList.of())
}
}
}