diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Defs.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Defs.kt index 2ac170a..740db6f 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Defs.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Defs.kt @@ -5,6 +5,7 @@ import com.republicate.kson.Json typealias ID = Int fun String.toID() = toInt() +fun String.toIDOrNull() = toIntOrNull() fun Number.toID() = toInt() fun Json.Object.getID(key: String) = getInt(key) fun Json.Array.getID(index: Int) = getInt(index) \ No newline at end of file diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Game.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Game.kt index 13920d5..af4bd8c 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Game.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Game.kt @@ -11,6 +11,7 @@ data class Game( var handicap: Int = 0, var result: Result = UNKNOWN ) { + companion object {} enum class Result(val symbol: Char) { UNKNOWN('?'), BLACK('b'), @@ -36,3 +37,11 @@ fun Game.toJson() = Json.Object( "h" to handicap, "r" to "${result.symbol}" ) + +fun Game.Companion.fromJson(json: Json.Object) = Game( + id = json.getID("id") ?: throw Error("missing game id"), + white = json.getID("white") ?: throw Error("missing white player"), + black = json.getID("black") ?: throw Error("missing black player"), + handicap = json.getInt("handicap") ?: 0, + result = json.getChar("result")?.let { Game.Result.fromSymbol(it) } ?: UNKNOWN +) diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt index b175afd..ebe853d 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt @@ -65,7 +65,9 @@ sealed class Tournament ( // games per id for each round private val games = mutableListOf>() - fun games(round: Int) = games.getOrNull(round - 1) ?: mutableMapOf() + fun games(round: Int) = games.getOrNull(round - 1) ?: + if (round > games.size) throw Error("invalid round") + else mutableMapOf().also { games.add(it) } fun lastRound() = games.size // standings criteria @@ -153,7 +155,7 @@ class TeamTournament( fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = null): Tournament<*> { val type = json.getString("type")?.uppercase()?.let { Tournament.Type.valueOf(it) } ?: default?.type ?: badRequest("missing type") // No clean way to avoid this redundancy - return if (type.playersNumber == 1) + val tournament = if (type.playersNumber == 1) StandardTournament( id = json.getInt("id") ?: default?.id ?: Store.nextTournamentId, type = type, @@ -189,6 +191,10 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"), pairing = json.getObject("pairing")?.let { Pairing.fromJson(it) } ?: default?.pairing ?: badRequest("missing pairing") ) + json["pairables"]?.let { pairables -> + + } + return tournament } fun Tournament<*>.toJson() = Json.Object( diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/store/FileStore.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/store/FileStore.kt index 7b749e7..f4d69cf 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/store/FileStore.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/store/FileStore.kt @@ -1,4 +1,111 @@ package org.jeudego.pairgoth.store -class FileStore { -} \ No newline at end of file +import com.republicate.kson.Json +import org.jeudego.pairgoth.model.Game +import org.jeudego.pairgoth.model.ID +import org.jeudego.pairgoth.model.Player +import org.jeudego.pairgoth.model.TeamTournament +import org.jeudego.pairgoth.model.Tournament +import org.jeudego.pairgoth.model.fromJson +import org.jeudego.pairgoth.model.getID +import org.jeudego.pairgoth.model.toID +import org.jeudego.pairgoth.model.toJson +import java.nio.file.Path +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* +import kotlin.io.path.readText +import kotlin.io.path.useDirectoryEntries + +private const val LEFT_PAD = 6 // left padding of IDs with '0' in filename +private fun Tournament<*>.filename() = "${id.toString().padStart(LEFT_PAD, '0')}-${shortName}.tour" + +class FileStore(pathStr: String): StoreImplementation { + companion object { + private val filenameRegex = Regex("^(\\d+)-.*\\.tour$") + private val timestampFormat: DateFormat = SimpleDateFormat("yyyyMMddHHmmss") + private val timestamp: String get() = timestampFormat.format(Date()) + } + + private val path = Path.of(pathStr).also { + val file = it.toFile() + if (!file.mkdirs() && !file.isDirectory) throw Error("Property pairgoth.store.file.path must be a directory") + } + + override fun getTournamentsIDs(): Set { + return path.useDirectoryEntries("*.tour") { entries -> + entries.mapNotNull { entry -> + filenameRegex.matchEntire(entry.fileName.toString())?.groupValues?.get(1)?.toID() + }.toSet() + } + } + + override fun addTournament(tournament: Tournament<*>) { + val filename = tournament.filename() + val file = path.resolve(filename).toFile() + if (file.exists()) throw Error("File $filename already exists") + val json = Json.MutableObject(tournament.toJson()) + json["players"] = Json.Array(tournament.players.values.map { it.toJson() }) + if (tournament is TeamTournament) { + json["teams"] = Json.Array(tournament.teams.values.map { it.toJson() }) + } + json["games"] = Json.Array((1..tournament.lastRound()).map { round -> tournament.games(round).values.map { it.toJson() } }); + file.printWriter().use { out -> + out.println(json.toPrettyString()) + } + } + + override fun getTournament(id: ID): Tournament<*>? { + val file = path.useDirectoryEntries("${id.toString().padStart(LEFT_PAD, '0')}-*.tour") { entries -> + entries.map { entry -> + entry.fileName.toString() + } + }.firstOrNull() ?: throw Error("no such tournament") + val json = Json.parse(path.resolve(file).readText())?.asObject() ?: throw Error("could not read tournament") + val tournament = Tournament.fromJson(json) + val players = json["players"] as Json.Array? ?: Json.Array() + tournament.players.putAll( + players.associate { + (it as Json.Object).let { player -> + Pair(player.getID("id") ?: throw Error("invalid tournament file"), Player.fromJson(player)) + } + } + ) + if (tournament is TeamTournament) { + val teams = json["teams"] as Json.Array? ?: Json.Array() + tournament.teams.putAll( + teams.associate { + (it as Json.Object).let { team -> + Pair(team.getID("id") ?: throw Error("invalid tournament file"), tournament.teamFromJson(team)) + } + } + ) + } + val games = json["games"] as Json.Array? ?: Json.Array() + (1..games.size).forEach { round -> + tournament.games(round).putAll( + games.associate { + (it as Json.Object).let { game -> + Pair(game.getID("id") ?: throw Error("invalid tournament file"), Game.fromJson(game)) + } + } + ) + } + return tournament + } + + override fun replaceTournament(tournament: Tournament<*>) { + val filename = tournament.filename() + val file = path.resolve(filename).toFile() + if (!file.exists()) throw Error("File $filename does not exist") + file.renameTo(path.resolve(filename + "-${timestamp}").toFile()) + addTournament(tournament) + } + + override fun deleteTournament(tournament: Tournament<*>) { + val filename = tournament.filename() + val file = path.resolve(filename).toFile() + if (!file.exists()) throw Error("File $filename does not exist") + file.renameTo(path.resolve(filename + "-${timestamp}").toFile()) + } +} diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/store/MemoryStore.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/store/MemoryStore.kt index e8b22fd..1d7f99e 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/store/MemoryStore.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/store/MemoryStore.kt @@ -1,4 +1,28 @@ package org.jeudego.pairgoth.store -class MemoryStore { -} \ No newline at end of file +import org.jeudego.pairgoth.model.ID +import org.jeudego.pairgoth.model.Tournament + +class MemoryStore: StoreImplementation { + private val tournaments = mutableMapOf>() + + override fun getTournamentsIDs(): Set = tournaments.keys + + override fun addTournament(tournament: Tournament<*>) { + if (tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} already exists") + tournaments[tournament.id] = tournament + } + + override fun getTournament(id: ID) = tournaments[id] + + override fun replaceTournament(tournament: Tournament<*>) { + if (!tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} not known") + tournaments[tournament.id] = tournament + } + + override fun deleteTournament(tournament: Tournament<*>) { + if (!tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} not known") + tournaments.remove(tournament.id) + + } +} diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt index f34a3d2..dd582df 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt @@ -1,38 +1,19 @@ package org.jeudego.pairgoth.store import org.jeudego.pairgoth.model.ID -import org.jeudego.pairgoth.model.Player import org.jeudego.pairgoth.model.Tournament import java.util.concurrent.atomic.AtomicInteger -import kotlin.math.E - -object Store { - private val _nextTournamentId = AtomicInteger() - private val _nextPlayerId = AtomicInteger() - private val _nextGameId = AtomicInteger() - val nextTournamentId get() = _nextTournamentId.incrementAndGet() - val nextPlayerId get() = _nextPlayerId.incrementAndGet() - val nextGameId get() = _nextGameId.incrementAndGet() - - private val tournaments = mutableMapOf>() - - fun addTournament(tournament: Tournament<*>) { - if (tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} already exists") - tournaments[tournament.id] = tournament - } - - fun getTournament(id: ID) = tournaments[id] - - fun getTournamentsIDs(): Set = tournaments.keys - - fun replaceTournament(tournament: Tournament<*>) { - if (!tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} not known") - tournaments[tournament.id] = tournament - } - - fun deleteTournament(tournament: Tournament<*>) { - if (!tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} not known") - tournaments.remove(tournament.id) +private fun createStoreImplementation(): StoreImplementation { + val storeProperty = System.getProperty("pairgoth.store") ?: "memory" + return when (storeProperty) { + "memory" -> MemoryStore() + "file" -> { + val filePath = System.getProperty("pairgoth.store.file.path") ?: "." + FileStore(filePath) + } + else -> throw Error("unknown store: $storeProperty") } } + +object Store: StoreImplementation by createStoreImplementation() diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/store/StoreImplementation.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/store/StoreImplementation.kt index e5bd17b..5a00cb0 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/store/StoreImplementation.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/store/StoreImplementation.kt @@ -1,4 +1,22 @@ package org.jeudego.pairgoth.store +import org.jeudego.pairgoth.model.ID +import org.jeudego.pairgoth.model.Tournament +import java.util.concurrent.atomic.AtomicInteger + +private val _nextTournamentId = AtomicInteger() +private val _nextPlayerId = AtomicInteger() +private val _nextGameId = AtomicInteger() + interface StoreImplementation { -} \ No newline at end of file + + val nextTournamentId get() = _nextTournamentId.incrementAndGet() + val nextPlayerId get() = _nextPlayerId.incrementAndGet() + val nextGameId get() = _nextGameId.incrementAndGet() + + fun getTournamentsIDs(): Set + fun addTournament(tournament: Tournament<*>) + fun getTournament(id: ID): Tournament<*>? + fun replaceTournament(tournament: Tournament<*>) + fun deleteTournament(tournament: Tournament<*>) +} diff --git a/webapp/src/main/webapp/WEB-INF/pairgoth.default.properties b/webapp/src/main/webapp/WEB-INF/pairgoth.default.properties index 3eb6b08..b0ebec3 100644 --- a/webapp/src/main/webapp/WEB-INF/pairgoth.default.properties +++ b/webapp/src/main/webapp/WEB-INF/pairgoth.default.properties @@ -2,6 +2,10 @@ webapp.env = dev webapp.url = http://localhost:8080 +# store +store = file +store.file.path = tournamentfiles + # smtp smtp.sender = smtp.host =