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] ) /** * Чтение и запись десайдеров. * * Десайдеры хранятся во внешнем HOCON-файле (admin.decidersFile, по умолчанию * "conf/deciders.conf"). Пока этот файл не существует, десайдеры читаются из * application.conf, поэтому приложение работает "из коробки". Как только админ * что-то сохраняет через админку, внешний файл становится единственным источником * правды. */ object DeciderRepository { 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 )) } } /** Все десайдеры. */ 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(" 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 } }