Files
Soulstorm-tournament-decider/app/repository/DeciderRepository.scala
T

195 lines
7.7 KiB
Scala

package repository
import com.typesafe.config.{Config, ConfigFactory}
import java.io.{File, PrintWriter}
import scala.collection.JavaConverters._
import scala.util.Try
/** Описание одной карты в маппуле десайдера. */
case class DeciderMapConfig(map: String, description: Option[String] = None)
/** Полное описание десайдера (турнира). */
case class Decider(
key: String,
name: String,
rules: String,
raceCount: Int,
existAdditionalRaces: Boolean,
existTextFieldRaces: Boolean,
description: Boolean,
maps: List[DeciderMapConfig],
/** Steam ID владельца: только он может редактировать и удалять десайдера. */
ownerSteamId: Option[String] = None
)
/**
* Чтение и запись десайдеров.
*
* Десайдеры хранятся во внешнем HOCON-файле (admin.decidersFile, по умолчанию
* "conf/deciders.conf"). Пока этот файл не существует, десайдеры читаются из
* application.conf, поэтому приложение работает "из коробки". Как только админ
* что-то сохраняет через админку, внешний файл становится единственным источником
* правды.
*/
object DeciderRepository {
/**
* Владелец по умолчанию. Записи без ownerSteamId (например, из старого
* application.conf) считаются принадлежащими этому Steam ID.
*/
val DefaultOwnerSteamId: String = "76561198137977374"
private def appConfig: Config = ConfigFactory.load()
/** Файл, в который админка пишет десайдеров. */
def file: File = {
val path = Try(appConfig.getString("admin.decidersFile")).getOrElse("conf/deciders.conf")
new File(path)
}
/** Десайдеры как объект Config (содержимое блока `deciders { ... }`). */
def decidersConfig: Config = {
val f = file
if (f.isFile) {
Try(ConfigFactory.parseFile(f)).toOption
.filter(_.hasPath("deciders"))
.map(_.getConfig("deciders"))
.getOrElse(fallback)
} else {
fallback
}
}
/** Переинициализировать кеш конфигуратора (вызывать после сохранения). */
def reload(): Unit = {
ConfigFactory.invalidateCaches()
}
private def fallback: Config =
if (appConfig.hasPath("deciders")) appConfig.getConfig("deciders") else ConfigFactory.empty()
/** Ключи всех десайдеров (отсортированные). */
def keys: Seq[String] = decidersConfig.root().keySet().asScala.toSeq.sorted
/** Один десайдер по ключу. */
def get(key: String): Option[Decider] = {
val c = decidersConfig
if (!c.hasPath(key)) None
else {
val d = c.getConfig(key)
val maps = d.getList("maps").asScala.toList.map { value =>
value.unwrapped() match {
case list: java.util.List[_] =>
val parts = list.asScala.map(_.toString).toList
DeciderMapConfig(parts.head, parts.lift(1).filter(_.nonEmpty))
case other =>
// Некоторые десайдеры заданы просто списком строк без описаний.
DeciderMapConfig(other.toString, None)
}
}
Some(Decider(
key = key,
name = Try(d.getString("name")).getOrElse(key),
rules = Try(d.getString("rules")).getOrElse(""),
raceCount = Try(d.getInt("raceCount")).getOrElse(1),
existAdditionalRaces = Try(d.getBoolean("existAdditionalRaces")).getOrElse(false),
existTextFieldRaces = Try(d.getBoolean("existTextFieldRaces")).getOrElse(false),
description = Try(d.getBoolean("description")).getOrElse(false),
maps = maps,
ownerSteamId = Try(d.getString("ownerSteamId")).toOption
.map(_.trim)
.filter(_.nonEmpty)
.orElse(Some(DefaultOwnerSteamId))
))
}
}
/** Все десайдеры. */
def all: Seq[Decider] = keys.flatMap(get)
/** Создаёт или обновляет десайдера и перезаписывает весь файл десайдеров. */
def save(decider: Decider): Unit = {
val updated = (all.filterNot(_.key == decider.key) :+ decider).sortBy(_.key)
write(updated)
}
/** Удаляет десайдера. Возвращает true, если десайдер существовал. */
def delete(key: String): Boolean = {
val current = all
if (current.exists(_.key == key)) {
write(current.filterNot(_.key == key))
true
} else {
false
}
}
private def write(deciders: Seq[Decider]): Unit = {
val sb = new StringBuilder
sb.append("# Конфигурация десайдеров.\n")
sb.append("# Файл управляется админкой (авторизация через Steam).\n")
sb.append("# Ручные правки могут быть перезаписаны при следующем сохранении.\n\n")
sb.append("deciders {\n")
deciders.foreach { d =>
sb.append(" ").append(quoteKey(d.key)).append(" {\n")
sb.append(" name = ").append(quote(d.name)).append('\n')
sb.append(" ownerSteamId = ")
.append(quote(d.ownerSteamId.getOrElse(DefaultOwnerSteamId)))
.append('\n')
sb.append(" raceCount = ").append(d.raceCount).append('\n')
if (d.existAdditionalRaces) sb.append(" existAdditionalRaces = true\n")
if (d.existTextFieldRaces) sb.append(" existTextFieldRaces = true\n")
if (d.description) sb.append(" description = true\n")
if (d.maps.isEmpty) {
sb.append(" maps = []\n")
} else {
sb.append(" maps = [\n")
d.maps.zipWithIndex.foreach { case (m, i) =>
sb.append(" ").append(renderMap(m))
if (i < d.maps.size - 1) sb.append(',')
sb.append('\n')
}
sb.append(" ]\n")
}
sb.append(" rules = ").append(quote(d.rules)).append('\n')
sb.append(" }\n")
}
sb.append("}\n")
val f = file
Option(f.getParentFile).foreach(_.mkdirs())
val writer = new PrintWriter(f, "UTF-8")
try writer.write(sb.toString) finally writer.close()
}
private def renderMap(m: DeciderMapConfig): String =
m.description.filter(_.nonEmpty) match {
case Some(desc) => s"[${quote(m.map)}, ${quote(desc)}]"
case None => s"[${quote(m.map)}]"
}
private def quoteKey(key: String): String =
if (key.matches("[A-Za-z0-9_-]+") && !key.forall(_.isDigit)) key else quote(key)
/** Рендерит строку как строку в формате JSON/HOCON. */
private def quote(value: String): String = {
val sb = new StringBuilder("\"")
value.foreach {
case '"' => sb.append("\\\"")
case '\\' => sb.append("\\\\")
case '\n' => sb.append("\\n")
case '\r' => sb.append("\\r")
case '\t' => sb.append("\\t")
// Литерал из обратного слэша и буквы u ломает компилятор Scala:
// такая последовательность обрабатывается как Unicode-escape ещё до лексера.
// Поэтому символы добавляем по отдельности, без escape-последовательности в исходнике.
case c if c < ' ' =>
sb.append(0x5c.toChar).append('u').append("%04x".format(c.toInt))
case c => sb.append(c)
}
sb.append('"')
sb.toString
}
}