From dda1b7e69ef89be95f612b62e84e28810f78a1c5 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Mon, 22 May 2023 18:23:33 +0200 Subject: [PATCH 1/3] Team / individual tournaments in progress --- .../pairgoth/api/PairgothApiHandler.kt | 2 +- .../org/jeudego/pairgoth/api/PlayerHandler.kt | 15 ++- .../jeudego/pairgoth/api/TournamentHandler.kt | 10 +- .../org/jeudego/pairgoth/ext/OpenGotha.kt | 10 +- .../org/jeudego/pairgoth/model/Pairing.kt | 17 +-- .../org/jeudego/pairgoth/model/Tournament.kt | 112 ++++++++++++++---- .../jeudego/pairgoth/pairing/SwissSolver.kt | 1 - .../org/jeudego/pairgoth/store/Store.kt | 8 +- .../org/jeudego/pairgoth/util/XmlFormat.kt | 34 +++--- .../org/jeudego/pairgoth/util/XmlUtils.kt | 12 +- .../org/jeudego/pairgoth/web/ApiServlet.kt | 4 +- .../org/jeudego/pairgoth/web/WebappManager.kt | 3 - 12 files changed, 148 insertions(+), 80 deletions(-) diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairgothApiHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairgothApiHandler.kt index ac7cd69..45fa50f 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairgothApiHandler.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairgothApiHandler.kt @@ -6,7 +6,7 @@ import javax.servlet.http.HttpServletRequest interface PairgothApiHandler: ApiHandler { - fun getTournament(request: HttpServletRequest): Tournament { + fun getTournament(request: HttpServletRequest): Tournament<*> { val tournamentId = getSelector(request)?.toIntOrNull() ?: ApiHandler.badRequest("invalid tournament id") return Store.getTournament(tournamentId) ?: ApiHandler.badRequest("unknown tournament id") } diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt index d6ac610..99a15f8 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt @@ -13,7 +13,7 @@ import javax.servlet.http.HttpServletResponse object PlayerHandler: PairgothApiHandler { override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? { - val tournament = getTournament(request) ?: badRequest("invalid tournament") + val tournament = getTournament(request) return when (val pid = getSubSelector(request)?.toIntOrNull()) { null -> tournament.pairables.values.map { it.toJson() }.toJsonArray() else -> tournament.pairables[pid]?.toJson() ?: badRequest("no player with id #${pid}") @@ -21,31 +21,30 @@ object PlayerHandler: PairgothApiHandler { } override fun post(request: HttpServletRequest): Json { - val tournament = getTournament(request) ?: badRequest("invalid tournament") + val tournament = getTournament(request) val payload = getObjectPayload(request) // player parsing (CB TODO - team handling, based on tournament type) val player = Player.fromJson(payload) - tournament.pairables[player.id] = player + tournament.players[player.id] = player Event.dispatch(playerAdded, Json.Object("tournament" to tournament.id, "data" to player.toJson())) return Json.Object("success" to true, "id" to player.id) } override fun put(request: HttpServletRequest): Json { - val tournament = getTournament(request) ?: badRequest("invalid tournament") + val tournament = getTournament(request) val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector") val player = tournament.pairables[id] ?: badRequest("invalid player id") val payload = getObjectPayload(request) val updated = Player.fromJson(payload, player as Player) - tournament.pairables[updated.id] = updated + tournament.players[updated.id] = updated Event.dispatch(playerUpdated, Json.Object("tournament" to tournament.id, "data" to player.toJson())) return Json.Object("success" to true) } override fun delete(request: HttpServletRequest): Json { - val tournament = getTournament(request) ?: badRequest("invalid tournament") + val tournament = getTournament(request) val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector") - val player = tournament.pairables[id] ?: badRequest("invalid player id") - tournament.pairables.remove(id) + tournament.players.remove(id) ?: badRequest("invalid player id") Event.dispatch(playerDeleted, Json.Object("tournament" to tournament.id, "data" to id)) return Json.Object("success" to true) } diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt index c1c612b..ed9c59f 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt @@ -4,6 +4,7 @@ import com.republicate.kson.Json import org.jeudego.pairgoth.api.ApiHandler.Companion.PAYLOAD_KEY import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.ext.OpenGotha +import org.jeudego.pairgoth.model.TeamTournament import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.fromJson import org.jeudego.pairgoth.model.toJson @@ -48,12 +49,15 @@ object TournamentHandler: PairgothApiHandler { override fun put(request: HttpServletRequest): Json { // BC TODO - some checks are needed here (cannot lower rounds number if games have been played in removed rounds, for instance) - val tournament = getTournament(request) ?: badRequest("missing or invalid tournament id") + val tournament = getTournament(request) val payload = getObjectPayload(request) // disallow changing type if (payload.getString("type")?.let { it != tournament.type.name } == true) badRequest("tournament type cannot be changed") val updated = Tournament.fromJson(payload, tournament) - updated.pairables.putAll(tournament.pairables) + updated.players.putAll(tournament.players) + if (tournament is TeamTournament && updated is TeamTournament) { + updated.teams.putAll(tournament.teams) + } updated.games.addAll(tournament.games) updated.criteria.addAll(tournament.criteria) Store.replaceTournament(updated) @@ -62,7 +66,7 @@ object TournamentHandler: PairgothApiHandler { } override fun delete(request: HttpServletRequest): Json { - val tournament = getTournament(request) ?: badRequest("missing or invalid tournament id") + val tournament = getTournament(request) Store.deleteTournament(tournament) Event.dispatch(tournamentDeleted, tournament.id) return Json.Object("success" to true) diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt index 861dfe2..b5f3171 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt @@ -7,6 +7,7 @@ import org.jeudego.pairgoth.model.MacMahon import org.jeudego.pairgoth.model.Pairable import org.jeudego.pairgoth.model.Player import org.jeudego.pairgoth.model.StandardByoyomi +import org.jeudego.pairgoth.model.StandardTournament import org.jeudego.pairgoth.model.SuddenDeath import org.jeudego.pairgoth.model.Swiss import org.jeudego.pairgoth.model.TimeSystem @@ -96,12 +97,12 @@ class OpenGothaFormat(xml: Element): XmlFormat(xml) { } object OpenGotha { - fun import(element: Element): Tournament { + fun import(element: Element): Tournament<*> { val imported = OpenGothaFormat(element) val genParams = imported.TournamentParameterSet.GeneralParameterSet val handParams = imported.TournamentParameterSet.HandicapParameterSet val pairingParams = imported.TournamentParameterSet.PairingParameterSet - val tournament = Tournament( + val tournament = StandardTournament( id = Store.nextTournamentId, type = Tournament.Type.INDIVIDUAL, // CB for now, TODO name = genParams.name, @@ -150,7 +151,7 @@ object OpenGotha { ).also { canonicMap.put("${player.name}${player.firstName}".uppercase(Locale.ENGLISH), it.id) } - }.associateByTo(tournament.pairables) { it.id } + }.associateByTo(tournament.players) { it.id } val gamesPerRound = imported.Games.groupBy { it.roundNumber }.values.map { @@ -177,7 +178,7 @@ object OpenGotha { } // TODO - bye player(s) - fun export(tournament: Tournament): String { + fun export(tournament: Tournament<*>): String { val xml = """ @@ -225,7 +226,6 @@ object OpenGotha { Game.Result.JIGO -> "RESULT_EQUAL" Game.Result.BOTHWIN -> "RESULT_BOTHWIN" Game.Result.BOTHLOOSE -> "RESULT_BOTHLOOSE" - else -> throw Error("unhandled game result") } }" roundNumber="${ round + 1 diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt index afad9c5..2abd8cb 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt @@ -21,20 +21,21 @@ sealed class Pairing(val type: PairingType, val weights: Weights = Weights()) { val color: Double = 100.0 // per color unbalancing ) - abstract fun pair(tournament: Tournament, round: Int, pairables: List): List + abstract fun pair(tournament: Tournament<*>, round: Int, pairables: List): List } +fun Tournament<*>.historyBefore(round: Int) = + if (games.isEmpty()) emptyList() + else games.slice(0 until round).flatMap { it.values } + class Swiss( var method: Method, var firstRoundMethod: Method = method ): Pairing(SWISS) { enum class Method { SPLIT_AND_FOLD, SPLIT_AND_RANDOM, SPLIT_AND_SLIP } - override fun pair(tournament: Tournament, round: Int, pairables: List): List { + override fun pair(tournament: Tournament<*>, round: Int, pairables: List): List { val actualMethod = if (round == 1) firstRoundMethod else method - val history = - if (tournament.games.isEmpty()) emptyList() - else tournament.games.slice(0 until round).flatMap { it.values } - return SwissSolver(history, pairables, weights, actualMethod).pair() + return SwissSolver(tournament.historyBefore(round), pairables, weights, actualMethod).pair() } } @@ -44,13 +45,13 @@ class MacMahon( ): Pairing(MACMAHON) { val groups = mutableListOf() - override fun pair(tournament: Tournament, round: Int, pairables: List): List { + override fun pair(tournament: Tournament<*>, round: Int, pairables: List): List { TODO() } } class RoundRobin: Pairing(ROUNDROBIN) { - override fun pair(tournament: Tournament, round: Int, pairables: List): List { + override fun pair(tournament: Tournament<*>, round: Int, pairables: List): List { TODO() } } 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 45afeda..b297afc 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt @@ -5,7 +5,7 @@ import kotlinx.datetime.LocalDate import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.store.Store -data class Tournament( +sealed class Tournament ( val id: Int, val type: Type, val name: String, @@ -38,8 +38,12 @@ data class Tournament( NBW, MMS, SOS, SOSOS, SODOS } + // players per id + abstract val players: MutableMap + // pairables per id - val pairables = mutableMapOf() + protected val _pairables = mutableMapOf() + val pairables: Map get() = _pairables // pairing fun pair(round: Int, pairables: List): List { @@ -49,7 +53,7 @@ data class Tournament( if (round > rounds) badRequest("too many rounds") val evenPairables = if (pairables.size % 2 == 0) pairables - else pairables.toMutableList()?.also { it.add(ByePlayer) } + else pairables.toMutableList().also { it.add(ByePlayer) } return pairing.pair(this, round, evenPairables) } @@ -64,27 +68,93 @@ data class Tournament( ) } +// standard tournament of individuals +class StandardTournament( + id: Int, + type: Tournament.Type, + name: String, + shortName: String, + startDate: LocalDate, + endDate: LocalDate, + country: String, + location: String, + online: Boolean, + timeSystem: TimeSystem, + rounds: Int, + pairing: Pairing, + rules: Rules = Rules.FRENCH, + gobanSize: Int = 19, + komi: Double = 7.5 +): Tournament(id, type, name, shortName, startDate, endDate, country, location, online, timeSystem, rounds, pairing, rules, gobanSize, komi) { + override val players get() = _pairables +} + +// team tournament +class TeamTournament( + id: Int, + type: Tournament.Type, + name: String, + shortName: String, + startDate: LocalDate, + endDate: LocalDate, + country: String, + location: String, + online: Boolean, + timeSystem: TimeSystem, + rounds: Int, + pairing: Pairing, + rules: Rules = Rules.FRENCH, + gobanSize: Int = 19, + komi: Double = 7.5 +): Tournament(id, type, name, shortName, startDate, endDate, country, location, online, timeSystem, rounds, pairing, rules, gobanSize, komi) { + override val players = mutableMapOf() + val teams: MutableMap = _pairables +} + // Serialization -fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament? = null) = Tournament( - id = json.getInt("id") ?: default?.id ?: Store.nextTournamentId, - type = json.getString("type")?.uppercase()?.let { Tournament.Type.valueOf(it) } ?: default?.type ?: badRequest("missing type"), - name = json.getString("name") ?: default?.name ?: badRequest("missing name"), - shortName = json.getString("shortName") ?: default?.shortName ?: badRequest("missing shortName"), - startDate = json.getLocalDate("startDate") ?: default?.startDate ?: badRequest("missing startDate"), - endDate = json.getLocalDate("endDate") ?: default?.endDate ?: badRequest("missing endDate"), - country = json.getString("country") ?: default?.country ?: badRequest("missing country"), - location = json.getString("location") ?: default?.location ?: badRequest("missing location"), - online = json.getBoolean("online") ?: default?.online ?: false, - komi = json.getDouble("komi") ?: default?.komi ?: 7.5, - rules = json.getString("rules")?.let { Rules.valueOf(it) } ?: default?.rules ?: Rules.FRENCH, - gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19, - timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"), - rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"), - pairing = json.getObject("pairing")?.let { Pairing.fromJson(it) } ?: default?.pairing ?: badRequest("missing pairing") -) +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) + StandardTournament( + id = json.getInt("id") ?: default?.id ?: Store.nextTournamentId, + type = type, + name = json.getString("name") ?: default?.name ?: badRequest("missing name"), + shortName = json.getString("shortName") ?: default?.shortName ?: badRequest("missing shortName"), + startDate = json.getLocalDate("startDate") ?: default?.startDate ?: badRequest("missing startDate"), + endDate = json.getLocalDate("endDate") ?: default?.endDate ?: badRequest("missing endDate"), + country = json.getString("country") ?: default?.country ?: badRequest("missing country"), + location = json.getString("location") ?: default?.location ?: badRequest("missing location"), + online = json.getBoolean("online") ?: default?.online ?: false, + komi = json.getDouble("komi") ?: default?.komi ?: 7.5, + rules = json.getString("rules")?.let { Rules.valueOf(it) } ?: default?.rules ?: Rules.FRENCH, + gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19, + timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"), + rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"), + pairing = json.getObject("pairing")?.let { Pairing.fromJson(it) } ?: default?.pairing ?: badRequest("missing pairing") + ) + else + TeamTournament( + id = json.getInt("id") ?: default?.id ?: Store.nextTournamentId, + type = type, + name = json.getString("name") ?: default?.name ?: badRequest("missing name"), + shortName = json.getString("shortName") ?: default?.shortName ?: badRequest("missing shortName"), + startDate = json.getLocalDate("startDate") ?: default?.startDate ?: badRequest("missing startDate"), + endDate = json.getLocalDate("endDate") ?: default?.endDate ?: badRequest("missing endDate"), + country = json.getString("country") ?: default?.country ?: badRequest("missing country"), + location = json.getString("location") ?: default?.location ?: badRequest("missing location"), + online = json.getBoolean("online") ?: default?.online ?: false, + komi = json.getDouble("komi") ?: default?.komi ?: 7.5, + rules = json.getString("rules")?.let { Rules.valueOf(it) } ?: default?.rules ?: Rules.FRENCH, + gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19, + timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"), + rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"), + pairing = json.getObject("pairing")?.let { Pairing.fromJson(it) } ?: default?.pairing ?: badRequest("missing pairing") + ) +} -fun Tournament.toJson() = Json.Object( +fun Tournament<*>.toJson() = Json.Object( "id" to id, "type" to type.name, "name" to name, diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/SwissSolver.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/SwissSolver.kt index adacc44..23e1fbf 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/SwissSolver.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/SwissSolver.kt @@ -29,7 +29,6 @@ class SwissSolver(history: List, pairables: List, weights: Pairi else abs(q.placeInGroup.first - (p.placeInGroup.second - p.placeInGroup.first)) * weights.place SPLIT_AND_RANDOM -> rand.nextDouble() * p.placeInGroup.second * weights.place SPLIT_AND_SLIP -> abs(abs(p.placeInGroup.first - q.placeInGroup.first) - p.placeInGroup.second) * weights.place - else -> throw Error("unhandled case") } } + (abs(p.colorBalance + 1) + abs(q.colorBalance - 1)) * weights.color } 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 846c4c2..d62cd8f 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt @@ -13,9 +13,9 @@ object Store { val nextPlayerId get() = _nextPlayerId.incrementAndGet() val nextGameId get() = _nextGameId.incrementAndGet() - private val tournaments = mutableMapOf() + private val tournaments = mutableMapOf>() - fun addTournament(tournament: Tournament) { + fun addTournament(tournament: Tournament<*>) { if (tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} already exists") tournaments[tournament.id] = tournament } @@ -24,12 +24,12 @@ object Store { fun getTournamentsIDs(): Set = tournaments.keys - fun replaceTournament(tournament: Tournament) { + 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) { + 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/util/XmlFormat.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/util/XmlFormat.kt index 8d3f026..1a16880 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/util/XmlFormat.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/util/XmlFormat.kt @@ -66,7 +66,7 @@ open class OptionalStringXmlDelegate(val xml: Element) { open class StringXmlDelegate(val xml: Element) { open operator fun getValue(thisRef: Any?, property: KProperty<*>): String = xml.childOrNull(property.name)?.value() ?: error(property.name) - open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { value?.let { xml.child(property.name).textContent = value } } + open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { value.let { xml.child(property.name).textContent = value } } } open class OptionalBooleanXmlDelegate(val xml: Element) { @@ -76,7 +76,7 @@ open class OptionalBooleanXmlDelegate(val xml: Element) { open class BooleanXmlDelegate(val xml: Element) { open operator fun getValue(thisRef: Any?, property: KProperty<*>): Boolean = Json.TypeUtils.toBoolean(xml.childOrNull(property.name)?.value()) ?: error(property.name) - open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) { value?.let { xml.child(property.name).textContent = value.toString() } } + open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) { value.let { xml.child(property.name).textContent = value.toString() } } } open class OptionalIntXmlDelegate(val xml: Element) { @@ -86,7 +86,7 @@ open class OptionalIntXmlDelegate(val xml: Element) { open class IntXmlDelegate(val xml: Element) { open operator fun getValue(thisRef: Any?, property: KProperty<*>): Int = Json.TypeUtils.toInt(xml.childOrNull(property.name)?.value()) ?: error(property.name) - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { value?.let { xml.child(property.name).textContent = value.toString() } } + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { value.let { xml.child(property.name).textContent = value.toString() } } } open class OptionalLongXmlDelegate(val xml: Element) { @@ -96,7 +96,7 @@ open class OptionalLongXmlDelegate(val xml: Element) { open class LongXmlDelegate(val xml: Element) { open operator fun getValue(thisRef: Any?, property: KProperty<*>): Long = Json.TypeUtils.toLong(xml.childOrNull(property.name)?.value()) ?: error(property.name) - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Long) { value?.let { xml.child(property.name).textContent = value.toString() } } + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Long) { value.let { xml.child(property.name).textContent = value.toString() } } } open class OptionalDoubleXmlDelegate(val xml: Element) { @@ -106,7 +106,7 @@ open class OptionalDoubleXmlDelegate(val xml: Element) { open class DoubleXmlDelegate(val xml: Element) { open operator fun getValue(thisRef: Any?, property: KProperty<*>): Double = Json.TypeUtils.toDouble(xml.childOrNull(property.name)?.value()) ?: error(property.name) - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Double) { value?.let { xml.child(property.name).textContent = value.toString() } } + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Double) { value.let { xml.child(property.name).textContent = value.toString() } } } open class OptionalEnumXmlDelegate> (val xml: Element, private val kclass: KClass) { @@ -115,7 +115,7 @@ open class OptionalEnumXmlDelegate> (val xml: Element, private val kc val xmlValue = xml.childOrNull(property.name)?.textContent return enumValues.firstOrNull() { it.name == xmlValue } } - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: E?) { value?.let { xml.child(property.name).textContent = value.toString() } } + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: E?) { value.let { xml.child(property.name).textContent = value.toString() } } } open class EnumXmlDelegate> (val xml: Element, private val kclass: KClass) { @@ -124,7 +124,7 @@ open class EnumXmlDelegate> (val xml: Element, private val kclass: KC val xmlValue = xml.childOrNull(property.name)?.textContent return enumValues.firstOrNull() { it.name == xmlValue } ?: error(property.name) } - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: E) { value?.let { xml.child(property.name).textContent = value.toString() } } + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: E) { value.let { xml.child(property.name).textContent = value.toString() } } } const val ISO_LOCAL_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss" @@ -153,13 +153,13 @@ internal fun dateTimeFormat(format: String): DateTimeFormatter { open class OptionalDateXmlDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATE_FORMAT) { private val format = dateTimeFormat(formatString) open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDate? = xml.childOrNull(property.name)?.value()?.let { LocalDate.parse(it /*, format */) } - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDate?) { value?.let { xml.child(property.name).textContent = value?.let { /* format.format(value)*/ value.toString() } } } + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDate?) { value?.let { xml.child(property.name).textContent = value.let { /* format.format(value)*/ value.toString() } } } } open class DateXmlDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATE_FORMAT) { private val format = dateTimeFormat(formatString) open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDate = xml.childOrNull(property.name)?.value()?.let { LocalDate.parse(it /*, format */) } ?: error(property.name) - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDate) { value?.let { xml.child(property.name).textContent = value?.let { /* format.format(value)*/ value.toString() } } } + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDate) { value.let { xml.child(property.name).textContent = value.let { /* format.format(value)*/ value.toString() } } } } open class OptionalDateTimeXmlDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATETIME_FORMAT) { @@ -172,7 +172,7 @@ open class OptionalDateTimeXmlDelegate(val xml: Element, formatString: String = return inputString?.let { LocalDateTime.parse(it /*, format*/) } // CB TODO format handling } //** - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDateTime?) { value?.let { xml.child(property.name).textContent = value?.let { value.toString() /* format.format(value)*/ } } } + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDateTime?) { value?.let { xml.child(property.name).textContent = value.let { value.toString() /* format.format(value)*/ } } } } open class DateTimeXmlDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATETIME_FORMAT) { @@ -185,7 +185,7 @@ open class DateTimeXmlDelegate(val xml: Element, formatString: String = ISO_LOCA return inputString?.let { LocalDateTime.parse(it /*, format*/) } ?: error(property.name) // CB TODO format handling } //** - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDateTime) { value?.let { xml.child(property.name).textContent = value?.let { value.toString() /* format.format(value)*/ } } } + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDateTime) { value.let { xml.child(property.name).textContent = value.let { value.toString() /* format.format(value)*/ } } } } fun KClass.instantiate(content: T): F = constructors.first().call(content) @@ -207,7 +207,7 @@ open class OptionalStringXmlAttrDelegate(val xml: Element) { open class StringXmlAttrDelegate(val xml: Element) { open operator fun getValue(thisRef: Any?, property: KProperty<*>): String = xml.attr(property.name) ?: error(property.name) - open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { value?.let { xml.setAttr(property.name, value) } } + open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { value.let { xml.setAttr(property.name, value) } } } open class OptionalBooleanXmlAttrDelegate(val xml: Element) { @@ -217,7 +217,7 @@ open class OptionalBooleanXmlAttrDelegate(val xml: Element) { open class BooleanXmlAttrDelegate(val xml: Element) { open operator fun getValue(thisRef: Any?, property: KProperty<*>): Boolean = xml.boolAttr(property.name) ?: error(property.name) - open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) { value?.let { xml.setAttr(property.name, value) } } + open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) { value.let { xml.setAttr(property.name, value) } } } open class OptionalIntXmlAttrDelegate(val xml: Element) { @@ -227,7 +227,7 @@ open class OptionalIntXmlAttrDelegate(val xml: Element) { open class IntXmlAttrDelegate(val xml: Element) { open operator fun getValue(thisRef: Any?, property: KProperty<*>): Int = xml.intAttr(property.name) ?: error(property.name) - open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { value?.let { xml.setAttr(property.name, value) } } + open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { value.let { xml.setAttr(property.name, value) } } } open class OptionalLongXmlAttrDelegate(val xml: Element) { @@ -237,7 +237,7 @@ open class OptionalLongXmlAttrDelegate(val xml: Element) { open class LongXmlAttrDelegate(val xml: Element) { open operator fun getValue(thisRef: Any?, property: KProperty<*>): Long = xml.longAttr(property.name) ?: error(property.name) - open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Long) { value?.let { xml.setAttr(property.name, value) } } + open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Long) { value.let { xml.setAttr(property.name, value) } } } open class OptionalDoubleXmlAttrDelegate(val xml: Element) { @@ -247,7 +247,7 @@ open class OptionalDoubleXmlAttrDelegate(val xml: Element) { open class DoubleXmlAttrDelegate(val xml: Element) { open operator fun getValue(thisRef: Any?, property: KProperty<*>): Double = xml.doubleAttr(property.name) ?: error(property.name) - open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Double) { value?.let { xml.setAttr(property.name, value) } } + open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Double) { value.let { xml.setAttr(property.name, value) } } } open class OptionalDateXmlAttrDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATE_FORMAT) { @@ -259,7 +259,7 @@ open class OptionalDateXmlAttrDelegate(val xml: Element, formatString: String = open class DateXmlAttrDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATE_FORMAT) { private val format = dateTimeFormat(formatString) open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDate = xml.attr(property.name)?.let { LocalDate.parse(it/*, format*/) } ?: error(property.name) - open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDate) { value?.let { xml.setAttr(property.name, value.toString() /* format.format(value) */) } } + open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDate) { value.let { xml.setAttr(property.name, value.toString() /* format.format(value) */) } } } // containers delegates diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/util/XmlUtils.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/util/XmlUtils.kt index f9fa6e8..f2de090 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/util/XmlUtils.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/util/XmlUtils.kt @@ -287,11 +287,9 @@ object XmlUtils { */ @Throws(XPathExpressionException::class) fun search(xpath: String?, context: Node?): NodeList { - var ret: NodeList? = null val xp = XPathFactory.newInstance().newXPath() val exp = xp.compile(xpath) - ret = exp.evaluate(context, XPathConstants.NODESET) as NodeList - return ret + return exp.evaluate(context, XPathConstants.NODESET) as NodeList } /** @@ -305,7 +303,7 @@ object XmlUtils { fun getNodes(xpath: String?, context: Node?): List { val ret: MutableList = ArrayList() val lst = search(xpath, context) - for (i in 0 until lst!!.length) { + for (i in 0 until lst.length) { ret.add(lst.item(i)) } return ret @@ -322,7 +320,7 @@ object XmlUtils { fun getElements(xpath: String?, context: Node?): List { val ret: MutableList = ArrayList() val lst = search(xpath, context) - for (i in 0 until lst!!.length) { + for (i in 0 until lst.length) { // will throw a ClassCastExpression if Node is not an Element, // that's what we want ret.add(lst.item(i) as Element) @@ -340,7 +338,7 @@ object XmlUtils { fun nodePath(n: Node): String { // declarations - var parent: Node? = null + var parent: Node? val hierarchy = Stack() val buffer = StringBuffer("/") @@ -528,7 +526,7 @@ fun Node.trimTextNodes() { val children: NodeList = getChildNodes() for (i in 0 until children.length) { val child = children.item(i) - if (child.nodeType === Node.TEXT_NODE) { + if (child.nodeType == Node.TEXT_NODE) { child.textContent = child.textContent.trim() } else child.trimTextNodes() diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt index 2d0834a..aea6753 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt @@ -103,7 +103,7 @@ class ApiServlet : HttpServlet() { } catch (apiException: ApiException) { reason = apiException.message ?: "unknown API error" - if (reason == null) error(response, apiException.code) else error( + error( request, response, apiException.code, @@ -190,7 +190,7 @@ class ApiServlet : HttpServlet() { // some API calls like opengotha import accept xml docs as body // CB TODO - limit to those calls try { - XmlUtils.parse(request.reader)?.let { payload: Element -> + XmlUtils.parse(request.reader).let { payload: Element -> request.setAttribute(ApiHandler.PAYLOAD_KEY, payload) logger.info(blue("<< (xml document)")) diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/web/WebappManager.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/WebappManager.kt index 60b2145..6a831f2 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/web/WebappManager.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/WebappManager.kt @@ -104,9 +104,6 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H override fun contextDestroyed(sce: ServletContextEvent) { logger.info("---------- Stopping Web Application ----------") - // overcome a Jetty's bug (v9.4.10.v20180503) whereas if a @WebListener is also listed in the descriptor - // it will be instanciated twice... - if (context == null) return val context = sce.servletContext for (service in webServices.keys) stopService(service, true) // ??? DriverManager.deregisterDriver(com.mysql.cj.jdbc.Driver ...); From ed7bcac3feb3b35eefc244aa11629ae4394091a5 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Tue, 23 May 2023 07:37:57 +0200 Subject: [PATCH 2/3] Team / individual tournaments almost ok --- .../org/jeudego/pairgoth/api/PlayerHandler.kt | 5 +- .../org/jeudego/pairgoth/api/TeamHandler.kt | 53 +++++++++++++++++++ .../org/jeudego/pairgoth/model/Pairable.kt | 26 --------- .../org/jeudego/pairgoth/model/Tournament.kt | 32 ++++++++++- .../org/jeudego/pairgoth/web/ApiServlet.kt | 2 + .../kotlin/org/jeudego/pairgoth/web/Event.kt | 3 ++ 6 files changed, 91 insertions(+), 30 deletions(-) create mode 100644 webapp/src/main/kotlin/org/jeudego/pairgoth/api/TeamHandler.kt diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt index 99a15f8..5a68180 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt @@ -23,7 +23,6 @@ object PlayerHandler: PairgothApiHandler { override fun post(request: HttpServletRequest): Json { val tournament = getTournament(request) val payload = getObjectPayload(request) - // player parsing (CB TODO - team handling, based on tournament type) val player = Player.fromJson(payload) tournament.players[player.id] = player Event.dispatch(playerAdded, Json.Object("tournament" to tournament.id, "data" to player.toJson())) @@ -33,9 +32,9 @@ object PlayerHandler: PairgothApiHandler { override fun put(request: HttpServletRequest): Json { val tournament = getTournament(request) val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector") - val player = tournament.pairables[id] ?: badRequest("invalid player id") + val player = tournament.players[id] ?: badRequest("invalid player id") val payload = getObjectPayload(request) - val updated = Player.fromJson(payload, player as Player) + val updated = Player.fromJson(payload, player) tournament.players[updated.id] = updated Event.dispatch(playerUpdated, Json.Object("tournament" to tournament.id, "data" to player.toJson())) return Json.Object("success" to true) diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TeamHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TeamHandler.kt new file mode 100644 index 0000000..e440a9d --- /dev/null +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TeamHandler.kt @@ -0,0 +1,53 @@ +package org.jeudego.pairgoth.api + +import com.republicate.kson.Json +import com.republicate.kson.toJsonArray +import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest +import org.jeudego.pairgoth.model.TeamTournament +import org.jeudego.pairgoth.web.Event +import org.jeudego.pairgoth.web.Event.* +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +object TeamHandler: PairgothApiHandler { + + override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? { + val tournament = getTournament(request) + if (tournament !is TeamTournament) badRequest("tournament is not a team tournament") + return when (val pid = getSubSelector(request)?.toIntOrNull()) { + null -> tournament.teams.values.map { it.toJson() }.toJsonArray() + else -> tournament.teams[pid]?.toJson() ?: badRequest("no team with id #${pid}") + } + } + + override fun post(request: HttpServletRequest): Json { + val tournament = getTournament(request) + if (tournament !is TeamTournament) badRequest("tournament is not a team tournament") + val payload = getObjectPayload(request) + val team = tournament.teamFromJson(payload) + tournament.teams[team.id] = team + Event.dispatch(teamAdded, Json.Object("tournament" to tournament.id, "data" to team.toJson())) + return Json.Object("success" to true, "id" to team.id) + } + + override fun put(request: HttpServletRequest): Json { + val tournament = getTournament(request) + if (tournament !is TeamTournament) badRequest("tournament is not a team tournament") + val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector") + val team = tournament.teams[id] ?: badRequest("invalid team id") + val payload = getObjectPayload(request) + val updated = tournament.teamFromJson(payload, team) + tournament.teams[updated.id] = updated + Event.dispatch(teamUpdated, Json.Object("tournament" to tournament.id, "data" to team.toJson())) + return Json.Object("success" to true) + } + + override fun delete(request: HttpServletRequest): Json { + val tournament = getTournament(request) + if (tournament !is TeamTournament) badRequest("tournament is not a team tournament") + val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid team selector") + tournament.teams.remove(id) ?: badRequest("invalid team id") + Event.dispatch(teamDeleted, Json.Object("tournament" to tournament.id, "data" to id)) + return Json.Object("success" to true) + } +} diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt index 72dddc7..ecc0e68 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt @@ -83,29 +83,3 @@ fun Player.Companion.fromJson(json: Json.Object, default: Player? = null) = Play if (it.isNotEmpty()) player.skip.addAll(it.map { id -> (id as Number).toInt() }) } } - -// Team - -class Team(id: Int, name: String): Pairable(id, name, 0, 0) { - companion object {} - val players = mutableSetOf() - override val rating: Int get() = if (players.isEmpty()) super.rating else (players.sumOf { player -> player.rating.toDouble() } / players.size).roundToInt() - override val rank: Int get() = if (players.isEmpty()) super.rank else (players.sumOf { player -> player.rank.toDouble() } / players.size).roundToInt() - override fun toJson() = Json.Object( - "id" to id, - "name" to name, - "players" to players.map { it.toJson() }.toJsonArray() - ) -} - -fun Team.Companion.fromJson(json: Json.Object) = Team( - id = json.getInt("id") ?: Store.nextPlayerId, - name = json.getString("name") ?: badRequest("missing name") -).apply { - json.getArray("players")?.let { arr -> - arr.map { - if (it != null && it is Json.Object) Player.fromJson(it) - else badRequest("invalid players array") - } - } ?: badRequest("missing players") -} 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 b297afc..b5ad1e6 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt @@ -1,9 +1,11 @@ package org.jeudego.pairgoth.model import com.republicate.kson.Json +import com.republicate.kson.toJsonArray import kotlinx.datetime.LocalDate import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.store.Store +import kotlin.math.roundToInt sealed class Tournament ( val id: Int, @@ -106,9 +108,37 @@ class TeamTournament( rules: Rules = Rules.FRENCH, gobanSize: Int = 19, komi: Double = 7.5 -): Tournament(id, type, name, shortName, startDate, endDate, country, location, online, timeSystem, rounds, pairing, rules, gobanSize, komi) { +): Tournament(id, type, name, shortName, startDate, endDate, country, location, online, timeSystem, rounds, pairing, rules, gobanSize, komi) { + companion object {} override val players = mutableMapOf() val teams: MutableMap = _pairables + + inner class Team(id: Int, name: String): Pairable(id, name, 0, 0) { + val playerIds = mutableSetOf() + val teamPlayers: Set get() = playerIds.mapNotNull { players[id] }.toSet() + override val rating: Int get() = if (players.isEmpty()) super.rating else (teamPlayers.sumOf { player -> player.rating.toDouble() } / players.size).roundToInt() + override val rank: Int get() = if (players.isEmpty()) super.rank else (teamPlayers.sumOf { player -> player.rank.toDouble() } / players.size).roundToInt() + val club: String? get() = players.map { club }.distinct().let { if (it.size == 1) it[0] else null } + val country: String? get() = players.map { country }.distinct().let { if (it.size == 1) it[0] else null } + override fun toJson() = Json.Object( + "id" to id, + "name" to name, + "players" to playerIds.toList().toJsonArray() + ) + } + + fun teamFromJson(json: Json.Object, default: TeamTournament.Team? = null) = Team( + id = json.getInt("id") ?: default?.id ?: Store.nextPlayerId, + name = json.getString("name") ?: default?.name ?: badRequest("missing name") + ).apply { + json.getArray("players")?.let { arr -> + arr.map { + if (it != null && it is Json.Object) Player.fromJson(it) + else badRequest("invalid players array") + } + } ?: badRequest("missing players") + } + } // Serialization diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt index aea6753..269fca1 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt @@ -5,6 +5,7 @@ import org.jeudego.pairgoth.api.ApiHandler import org.jeudego.pairgoth.api.PairingHandler import org.jeudego.pairgoth.api.PlayerHandler import org.jeudego.pairgoth.api.ResultsHandler +import org.jeudego.pairgoth.api.TeamHandler import org.jeudego.pairgoth.api.TournamentHandler import org.jeudego.pairgoth.util.Colorizer.blue import org.jeudego.pairgoth.util.Colorizer.green @@ -86,6 +87,7 @@ class ApiServlet : HttpServlet() { "part" -> PlayerHandler "pair" -> PairingHandler "res" -> ResultsHandler + "team" -> TeamHandler else -> ApiHandler.badRequest("unknown sub-entity: $subEntity") } "player" -> PlayerHandler diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/web/Event.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/Event.kt index 2f46f56..2949aaf 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/web/Event.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/Event.kt @@ -10,6 +10,9 @@ enum class Event { playerAdded, playerUpdated, playerDeleted, + teamAdded, + teamUpdated, + teamDeleted, gamesAdded, gamesDeleted, resultUpdated, From 5e9ce3a9f82b2648161aa0324b79017f0cd2997a Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Tue, 23 May 2023 09:34:31 +0200 Subject: [PATCH 3/3] Team basic unit tests ok --- .../jeudego/pairgoth/api/PairingHandler.kt | 2 +- .../org/jeudego/pairgoth/model/Tournament.kt | 4 +- webapp/src/test/kotlin/BasicTests.kt | 45 +++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt index 9ba2727..445d55b 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt @@ -18,7 +18,7 @@ object PairingHandler: PairgothApiHandler { val playing = (tournament.games.getOrNull(round)?.values ?: emptyList()).flatMap { listOf(it.black, it.white) }.toSet() - return tournament.pairables.values.filter { !it.skip.contains(round) && !playing.contains(it.id) }.toJsonArray() + return tournament.pairables.values.filter { !it.skip.contains(round) && !playing.contains(it.id) }.map { it.id }.toJsonArray() } override fun post(request: HttpServletRequest): Json { 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 b5ad1e6..469f685 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt @@ -132,8 +132,8 @@ class TeamTournament( name = json.getString("name") ?: default?.name ?: badRequest("missing name") ).apply { json.getArray("players")?.let { arr -> - arr.map { - if (it != null && it is Json.Object) Player.fromJson(it) + arr.mapTo(playerIds) { + if (it != null && it is Number) it.toInt().also { id -> players.containsKey(id) } else badRequest("invalid players array") } } ?: badRequest("missing players") diff --git a/webapp/src/test/kotlin/BasicTests.kt b/webapp/src/test/kotlin/BasicTests.kt index 02690c2..c76de8d 100644 --- a/webapp/src/test/kotlin/BasicTests.kt +++ b/webapp/src/test/kotlin/BasicTests.kt @@ -5,6 +5,7 @@ import org.junit.jupiter.api.MethodOrderer.Alphanumeric import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import java.util.Objects import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -34,6 +35,27 @@ class BasicTests: TestBase() { ) ) + val aTeamTournament = Json.Object( + "type" to "TEAM2", + "name" to "Mon Tournoi par équipes", + "shortName" to "mon-tournoi-par-equipes", + "startDate" to "2023-05-20", + "endDate" to "2023-05-23", + "country" to "FR", + "location" to "Marseille", + "online" to true, + "timeSystem" to Json.Object( + "type" to "FISCHER", + "mainTime" to 1200, + "increment" to 10 + ), + "rounds" to 2, + "pairing" to Json.Object( + "type" to "SWISS", + "method" to "SPLIT_AND_RANDOM" + ) + ) + val aPlayer = Json.Object( "name" to "Burma", "firstname" to "Nestor", @@ -98,4 +120,27 @@ class BasicTests: TestBase() { "[{\"id\":1,\"w\":2,\"b\":1,\"r\":\"?\"}]") assertTrue(possibleResults.contains(games.toString()), "pairing differs") } + + @Test + fun `006 team tournament`() { + var resp = TestAPI.post("/api/tour", aTeamTournament) as Json.Object + assertTrue(resp.getBoolean("success") == true, "expecting success") + assertEquals(2, resp.getInt("id"), "expecting id #2 for new tournament") + resp = TestAPI.post("api/tour/2/part", aPlayer) as Json.Object + assertTrue(resp.getBoolean("success") == true, "expecting success") + assertEquals(3, resp.getInt("id"), "expecting id #3 for new player") + resp = TestAPI.post("api/tour/2/part", anotherPlayer) as Json.Object + assertTrue(resp.getBoolean("success") == true, "expecting success") + assertEquals(4, resp.getInt("id"), "expecting id #{ for new player") + assertTrue(resp.getBoolean("success") == true, "expecting success") + var arr = TestAPI.get("/api/tour/2/pair/1") as Json.Array + assertEquals("[]", arr.toString(), "expecting an empty array") + resp = TestAPI.post("api/tour/2/team", Json.parse("""{ "name":"The Buffallos", "players":[3, 4] }""") as Json.Object) as Json.Object + assertTrue(resp.getBoolean("success") == true, "expecting success") + assertEquals(5, resp.getInt("id"), "expecting team id #5") + resp = TestAPI.get("api/tour/2/team/5") as Json.Object + assertEquals("""{"id":5,"name":"The Buffallos","players":[3,4]}""", resp.toString(), "expecting team description") + arr = TestAPI.get("/api/tour/2/pair/1") as Json.Array + assertEquals("[5]", arr.toString(), "expecting a singleton array") + } }