File storage in progress
This commit is contained in:
@@ -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)
|
@@ -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
|
||||
)
|
||||
|
@@ -65,7 +65,9 @@ sealed class Tournament <P: Pairable>(
|
||||
// games per id for each round
|
||||
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
|
||||
|
||||
// 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(
|
||||
|
@@ -1,4 +1,111 @@
|
||||
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())
|
||||
}
|
||||
}
|
@@ -1,4 +1,28 @@
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
@@ -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<ID, Tournament<*>>()
|
||||
|
||||
fun addTournament(tournament: Tournament<*>) {
|
||||
if (tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} already exists")
|
||||
tournaments[tournament.id] = tournament
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
else -> throw Error("unknown store: $storeProperty")
|
||||
}
|
||||
}
|
||||
|
||||
object Store: StoreImplementation by createStoreImplementation()
|
||||
|
@@ -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 {
|
||||
|
||||
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<*>)
|
||||
}
|
@@ -2,6 +2,10 @@
|
||||
webapp.env = dev
|
||||
webapp.url = http://localhost:8080
|
||||
|
||||
# store
|
||||
store = file
|
||||
store.file.path = tournamentfiles
|
||||
|
||||
# smtp
|
||||
smtp.sender =
|
||||
smtp.host =
|
||||
|
Reference in New Issue
Block a user