279 lines
7.1 KiB
Kotlin
279 lines
7.1 KiB
Kotlin
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())
|
||
}
|
||
}
|
||
}
|