File storage in progress

This commit is contained in:
Claude Brisson
2023-06-06 10:58:40 +02:00
parent 71bc333c93
commit 7c82dad7bc
8 changed files with 187 additions and 37 deletions

View File

@@ -5,6 +5,7 @@ import com.republicate.kson.Json
typealias ID = Int typealias ID = Int
fun String.toID() = toInt() fun String.toID() = toInt()
fun String.toIDOrNull() = toIntOrNull()
fun Number.toID() = toInt() fun Number.toID() = toInt()
fun Json.Object.getID(key: String) = getInt(key) fun Json.Object.getID(key: String) = getInt(key)
fun Json.Array.getID(index: Int) = getInt(index) fun Json.Array.getID(index: Int) = getInt(index)

View File

@@ -11,6 +11,7 @@ data class Game(
var handicap: Int = 0, var handicap: Int = 0,
var result: Result = UNKNOWN var result: Result = UNKNOWN
) { ) {
companion object {}
enum class Result(val symbol: Char) { enum class Result(val symbol: Char) {
UNKNOWN('?'), UNKNOWN('?'),
BLACK('b'), BLACK('b'),
@@ -36,3 +37,11 @@ fun Game.toJson() = Json.Object(
"h" to handicap, "h" to handicap,
"r" to "${result.symbol}" "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
)

View File

@@ -65,7 +65,9 @@ sealed class Tournament <P: Pairable>(
// games per id for each round // games per id for each round
private val games = mutableListOf<MutableMap<ID, Game>>() private val games = mutableListOf<MutableMap<ID, Game>>()
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<ID, Game>().also { games.add(it) }
fun lastRound() = games.size fun lastRound() = games.size
// standings criteria // standings criteria
@@ -153,7 +155,7 @@ class TeamTournament(
fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = null): Tournament<*> { 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") val type = json.getString("type")?.uppercase()?.let { Tournament.Type.valueOf(it) } ?: default?.type ?: badRequest("missing type")
// No clean way to avoid this redundancy // No clean way to avoid this redundancy
return if (type.playersNumber == 1) val tournament = if (type.playersNumber == 1)
StandardTournament( StandardTournament(
id = json.getInt("id") ?: default?.id ?: Store.nextTournamentId, id = json.getInt("id") ?: default?.id ?: Store.nextTournamentId,
type = type, 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"), rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"),
pairing = json.getObject("pairing")?.let { Pairing.fromJson(it) } ?: default?.pairing ?: badRequest("missing pairing") pairing = json.getObject("pairing")?.let { Pairing.fromJson(it) } ?: default?.pairing ?: badRequest("missing pairing")
) )
json["pairables"]?.let { pairables ->
}
return tournament
} }
fun Tournament<*>.toJson() = Json.Object( fun Tournament<*>.toJson() = Json.Object(

View File

@@ -1,4 +1,111 @@
package org.jeudego.pairgoth.store package org.jeudego.pairgoth.store
class FileStore { 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<ID> {
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())
}
}

View File

@@ -1,4 +1,28 @@
package org.jeudego.pairgoth.store package org.jeudego.pairgoth.store
class MemoryStore { import org.jeudego.pairgoth.model.ID
} import org.jeudego.pairgoth.model.Tournament
class MemoryStore: StoreImplementation {
private val tournaments = mutableMapOf<ID, Tournament<*>>()
override fun getTournamentsIDs(): Set<ID> = 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)
}
}

View File

@@ -1,38 +1,19 @@
package org.jeudego.pairgoth.store package org.jeudego.pairgoth.store
import org.jeudego.pairgoth.model.ID import org.jeudego.pairgoth.model.ID
import org.jeudego.pairgoth.model.Player
import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.Tournament
import java.util.concurrent.atomic.AtomicInteger 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<ID, Tournament<*>>()
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<ID> = 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()

View File

@@ -1,4 +1,22 @@
package org.jeudego.pairgoth.store 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 { interface StoreImplementation {
}
val nextTournamentId get() = _nextTournamentId.incrementAndGet()
val nextPlayerId get() = _nextPlayerId.incrementAndGet()
val nextGameId get() = _nextGameId.incrementAndGet()
fun getTournamentsIDs(): Set<ID>
fun addTournament(tournament: Tournament<*>)
fun getTournament(id: ID): Tournament<*>?
fun replaceTournament(tournament: Tournament<*>)
fun deleteTournament(tournament: Tournament<*>)
}

View File

@@ -2,6 +2,10 @@
webapp.env = dev webapp.env = dev
webapp.url = http://localhost:8080 webapp.url = http://localhost:8080
# store
store = file
store.file.path = tournamentfiles
# smtp # smtp
smtp.sender = smtp.sender =
smtp.host = smtp.host =