Merge branch 'teams'
This commit is contained in:
@@ -6,7 +6,7 @@ import javax.servlet.http.HttpServletRequest
|
|||||||
|
|
||||||
interface PairgothApiHandler: ApiHandler {
|
interface PairgothApiHandler: ApiHandler {
|
||||||
|
|
||||||
fun getTournament(request: HttpServletRequest): Tournament {
|
fun getTournament(request: HttpServletRequest): Tournament<*> {
|
||||||
val tournamentId = getSelector(request)?.toIntOrNull() ?: ApiHandler.badRequest("invalid tournament id")
|
val tournamentId = getSelector(request)?.toIntOrNull() ?: ApiHandler.badRequest("invalid tournament id")
|
||||||
return Store.getTournament(tournamentId) ?: ApiHandler.badRequest("unknown tournament id")
|
return Store.getTournament(tournamentId) ?: ApiHandler.badRequest("unknown tournament id")
|
||||||
}
|
}
|
||||||
|
@@ -18,7 +18,7 @@ object PairingHandler: PairgothApiHandler {
|
|||||||
val playing = (tournament.games.getOrNull(round)?.values ?: emptyList()).flatMap {
|
val playing = (tournament.games.getOrNull(round)?.values ?: emptyList()).flatMap {
|
||||||
listOf(it.black, it.white)
|
listOf(it.black, it.white)
|
||||||
}.toSet()
|
}.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 {
|
override fun post(request: HttpServletRequest): Json {
|
||||||
|
@@ -13,7 +13,7 @@ import javax.servlet.http.HttpServletResponse
|
|||||||
object PlayerHandler: PairgothApiHandler {
|
object PlayerHandler: PairgothApiHandler {
|
||||||
|
|
||||||
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
|
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()) {
|
return when (val pid = getSubSelector(request)?.toIntOrNull()) {
|
||||||
null -> tournament.pairables.values.map { it.toJson() }.toJsonArray()
|
null -> tournament.pairables.values.map { it.toJson() }.toJsonArray()
|
||||||
else -> tournament.pairables[pid]?.toJson() ?: badRequest("no player with id #${pid}")
|
else -> tournament.pairables[pid]?.toJson() ?: badRequest("no player with id #${pid}")
|
||||||
@@ -21,31 +21,29 @@ object PlayerHandler: PairgothApiHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun post(request: HttpServletRequest): Json {
|
override fun post(request: HttpServletRequest): Json {
|
||||||
val tournament = getTournament(request) ?: badRequest("invalid tournament")
|
val tournament = getTournament(request)
|
||||||
val payload = getObjectPayload(request)
|
val payload = getObjectPayload(request)
|
||||||
// player parsing (CB TODO - team handling, based on tournament type)
|
|
||||||
val player = Player.fromJson(payload)
|
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()))
|
Event.dispatch(playerAdded, Json.Object("tournament" to tournament.id, "data" to player.toJson()))
|
||||||
return Json.Object("success" to true, "id" to player.id)
|
return Json.Object("success" to true, "id" to player.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun put(request: HttpServletRequest): Json {
|
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 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 payload = getObjectPayload(request)
|
||||||
val updated = Player.fromJson(payload, player as Player)
|
val updated = Player.fromJson(payload, player)
|
||||||
tournament.pairables[updated.id] = updated
|
tournament.players[updated.id] = updated
|
||||||
Event.dispatch(playerUpdated, Json.Object("tournament" to tournament.id, "data" to player.toJson()))
|
Event.dispatch(playerUpdated, Json.Object("tournament" to tournament.id, "data" to player.toJson()))
|
||||||
return Json.Object("success" to true)
|
return Json.Object("success" to true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun delete(request: HttpServletRequest): Json {
|
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 id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector")
|
||||||
val player = tournament.pairables[id] ?: badRequest("invalid player id")
|
tournament.players.remove(id) ?: badRequest("invalid player id")
|
||||||
tournament.pairables.remove(id)
|
|
||||||
Event.dispatch(playerDeleted, Json.Object("tournament" to tournament.id, "data" to id))
|
Event.dispatch(playerDeleted, Json.Object("tournament" to tournament.id, "data" to id))
|
||||||
return Json.Object("success" to true)
|
return Json.Object("success" to true)
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
@@ -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.PAYLOAD_KEY
|
||||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||||
import org.jeudego.pairgoth.ext.OpenGotha
|
import org.jeudego.pairgoth.ext.OpenGotha
|
||||||
|
import org.jeudego.pairgoth.model.TeamTournament
|
||||||
import org.jeudego.pairgoth.model.Tournament
|
import org.jeudego.pairgoth.model.Tournament
|
||||||
import org.jeudego.pairgoth.model.fromJson
|
import org.jeudego.pairgoth.model.fromJson
|
||||||
import org.jeudego.pairgoth.model.toJson
|
import org.jeudego.pairgoth.model.toJson
|
||||||
@@ -48,12 +49,15 @@ object TournamentHandler: PairgothApiHandler {
|
|||||||
|
|
||||||
override fun put(request: HttpServletRequest): Json {
|
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)
|
// 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)
|
val payload = getObjectPayload(request)
|
||||||
// disallow changing type
|
// disallow changing type
|
||||||
if (payload.getString("type")?.let { it != tournament.type.name } == true) badRequest("tournament type cannot be changed")
|
if (payload.getString("type")?.let { it != tournament.type.name } == true) badRequest("tournament type cannot be changed")
|
||||||
val updated = Tournament.fromJson(payload, tournament)
|
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.games.addAll(tournament.games)
|
||||||
updated.criteria.addAll(tournament.criteria)
|
updated.criteria.addAll(tournament.criteria)
|
||||||
Store.replaceTournament(updated)
|
Store.replaceTournament(updated)
|
||||||
@@ -62,7 +66,7 @@ object TournamentHandler: PairgothApiHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun delete(request: HttpServletRequest): Json {
|
override fun delete(request: HttpServletRequest): Json {
|
||||||
val tournament = getTournament(request) ?: badRequest("missing or invalid tournament id")
|
val tournament = getTournament(request)
|
||||||
Store.deleteTournament(tournament)
|
Store.deleteTournament(tournament)
|
||||||
Event.dispatch(tournamentDeleted, tournament.id)
|
Event.dispatch(tournamentDeleted, tournament.id)
|
||||||
return Json.Object("success" to true)
|
return Json.Object("success" to true)
|
||||||
|
@@ -7,6 +7,7 @@ import org.jeudego.pairgoth.model.MacMahon
|
|||||||
import org.jeudego.pairgoth.model.Pairable
|
import org.jeudego.pairgoth.model.Pairable
|
||||||
import org.jeudego.pairgoth.model.Player
|
import org.jeudego.pairgoth.model.Player
|
||||||
import org.jeudego.pairgoth.model.StandardByoyomi
|
import org.jeudego.pairgoth.model.StandardByoyomi
|
||||||
|
import org.jeudego.pairgoth.model.StandardTournament
|
||||||
import org.jeudego.pairgoth.model.SuddenDeath
|
import org.jeudego.pairgoth.model.SuddenDeath
|
||||||
import org.jeudego.pairgoth.model.Swiss
|
import org.jeudego.pairgoth.model.Swiss
|
||||||
import org.jeudego.pairgoth.model.TimeSystem
|
import org.jeudego.pairgoth.model.TimeSystem
|
||||||
@@ -96,12 +97,12 @@ class OpenGothaFormat(xml: Element): XmlFormat(xml) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object OpenGotha {
|
object OpenGotha {
|
||||||
fun import(element: Element): Tournament {
|
fun import(element: Element): Tournament<*> {
|
||||||
val imported = OpenGothaFormat(element)
|
val imported = OpenGothaFormat(element)
|
||||||
val genParams = imported.TournamentParameterSet.GeneralParameterSet
|
val genParams = imported.TournamentParameterSet.GeneralParameterSet
|
||||||
val handParams = imported.TournamentParameterSet.HandicapParameterSet
|
val handParams = imported.TournamentParameterSet.HandicapParameterSet
|
||||||
val pairingParams = imported.TournamentParameterSet.PairingParameterSet
|
val pairingParams = imported.TournamentParameterSet.PairingParameterSet
|
||||||
val tournament = Tournament(
|
val tournament = StandardTournament(
|
||||||
id = Store.nextTournamentId,
|
id = Store.nextTournamentId,
|
||||||
type = Tournament.Type.INDIVIDUAL, // CB for now, TODO
|
type = Tournament.Type.INDIVIDUAL, // CB for now, TODO
|
||||||
name = genParams.name,
|
name = genParams.name,
|
||||||
@@ -150,7 +151,7 @@ object OpenGotha {
|
|||||||
).also {
|
).also {
|
||||||
canonicMap.put("${player.name}${player.firstName}".uppercase(Locale.ENGLISH), it.id)
|
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 {
|
val gamesPerRound = imported.Games.groupBy {
|
||||||
it.roundNumber
|
it.roundNumber
|
||||||
}.values.map {
|
}.values.map {
|
||||||
@@ -177,7 +178,7 @@ object OpenGotha {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO - bye player(s)
|
// TODO - bye player(s)
|
||||||
fun export(tournament: Tournament): String {
|
fun export(tournament: Tournament<*>): String {
|
||||||
val xml = """
|
val xml = """
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<Tournament dataVersion="201" externalIPAddress="88.122.144.219" fullVersionNumber="3.51" runningMode="SAL" saveDT="20210111180800">
|
<Tournament dataVersion="201" externalIPAddress="88.122.144.219" fullVersionNumber="3.51" runningMode="SAL" saveDT="20210111180800">
|
||||||
@@ -225,7 +226,6 @@ object OpenGotha {
|
|||||||
Game.Result.JIGO -> "RESULT_EQUAL"
|
Game.Result.JIGO -> "RESULT_EQUAL"
|
||||||
Game.Result.BOTHWIN -> "RESULT_BOTHWIN"
|
Game.Result.BOTHWIN -> "RESULT_BOTHWIN"
|
||||||
Game.Result.BOTHLOOSE -> "RESULT_BOTHLOOSE"
|
Game.Result.BOTHLOOSE -> "RESULT_BOTHLOOSE"
|
||||||
else -> throw Error("unhandled game result")
|
|
||||||
}
|
}
|
||||||
}" roundNumber="${
|
}" roundNumber="${
|
||||||
round + 1
|
round + 1
|
||||||
|
@@ -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() })
|
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<Player>()
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
@@ -21,20 +21,21 @@ sealed class Pairing(val type: PairingType, val weights: Weights = Weights()) {
|
|||||||
val color: Double = 100.0 // per color unbalancing
|
val color: Double = 100.0 // per color unbalancing
|
||||||
)
|
)
|
||||||
|
|
||||||
abstract fun pair(tournament: Tournament, round: Int, pairables: List<Pairable>): List<Game>
|
abstract fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Tournament<*>.historyBefore(round: Int) =
|
||||||
|
if (games.isEmpty()) emptyList()
|
||||||
|
else games.slice(0 until round).flatMap { it.values }
|
||||||
|
|
||||||
class Swiss(
|
class Swiss(
|
||||||
var method: Method,
|
var method: Method,
|
||||||
var firstRoundMethod: Method = method
|
var firstRoundMethod: Method = method
|
||||||
): Pairing(SWISS) {
|
): Pairing(SWISS) {
|
||||||
enum class Method { SPLIT_AND_FOLD, SPLIT_AND_RANDOM, SPLIT_AND_SLIP }
|
enum class Method { SPLIT_AND_FOLD, SPLIT_AND_RANDOM, SPLIT_AND_SLIP }
|
||||||
override fun pair(tournament: Tournament, round: Int, pairables: List<Pairable>): List<Game> {
|
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
|
||||||
val actualMethod = if (round == 1) firstRoundMethod else method
|
val actualMethod = if (round == 1) firstRoundMethod else method
|
||||||
val history =
|
return SwissSolver(tournament.historyBefore(round), pairables, weights, actualMethod).pair()
|
||||||
if (tournament.games.isEmpty()) emptyList()
|
|
||||||
else tournament.games.slice(0 until round).flatMap { it.values }
|
|
||||||
return SwissSolver(history, pairables, weights, actualMethod).pair()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,13 +45,13 @@ class MacMahon(
|
|||||||
): Pairing(MACMAHON) {
|
): Pairing(MACMAHON) {
|
||||||
val groups = mutableListOf<Int>()
|
val groups = mutableListOf<Int>()
|
||||||
|
|
||||||
override fun pair(tournament: Tournament, round: Int, pairables: List<Pairable>): List<Game> {
|
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
|
||||||
TODO()
|
TODO()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RoundRobin: Pairing(ROUNDROBIN) {
|
class RoundRobin: Pairing(ROUNDROBIN) {
|
||||||
override fun pair(tournament: Tournament, round: Int, pairables: List<Pairable>): List<Game> {
|
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
|
||||||
TODO()
|
TODO()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
package org.jeudego.pairgoth.model
|
package org.jeudego.pairgoth.model
|
||||||
|
|
||||||
import com.republicate.kson.Json
|
import com.republicate.kson.Json
|
||||||
|
import com.republicate.kson.toJsonArray
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||||
import org.jeudego.pairgoth.store.Store
|
import org.jeudego.pairgoth.store.Store
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
data class Tournament(
|
sealed class Tournament <P: Pairable>(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val type: Type,
|
val type: Type,
|
||||||
val name: String,
|
val name: String,
|
||||||
@@ -38,8 +40,12 @@ data class Tournament(
|
|||||||
NBW, MMS, SOS, SOSOS, SODOS
|
NBW, MMS, SOS, SOSOS, SODOS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// players per id
|
||||||
|
abstract val players: MutableMap<Int, Player>
|
||||||
|
|
||||||
// pairables per id
|
// pairables per id
|
||||||
val pairables = mutableMapOf<Int, Pairable>()
|
protected val _pairables = mutableMapOf<Int, P>()
|
||||||
|
val pairables: Map<Int, Pairable> get() = _pairables
|
||||||
|
|
||||||
// pairing
|
// pairing
|
||||||
fun pair(round: Int, pairables: List<Pairable>): List<Game> {
|
fun pair(round: Int, pairables: List<Pairable>): List<Game> {
|
||||||
@@ -49,7 +55,7 @@ data class Tournament(
|
|||||||
if (round > rounds) badRequest("too many rounds")
|
if (round > rounds) badRequest("too many rounds")
|
||||||
val evenPairables =
|
val evenPairables =
|
||||||
if (pairables.size % 2 == 0) pairables
|
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)
|
return pairing.pair(this, round, evenPairables)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,27 +70,121 @@ 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<Player>(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<TeamTournament.Team>(id, type, name, shortName, startDate, endDate, country, location, online, timeSystem, rounds, pairing, rules, gobanSize, komi) {
|
||||||
|
companion object {}
|
||||||
|
override val players = mutableMapOf<Int, Player>()
|
||||||
|
val teams: MutableMap<Int, Team> = _pairables
|
||||||
|
|
||||||
|
inner class Team(id: Int, name: String): Pairable(id, name, 0, 0) {
|
||||||
|
val playerIds = mutableSetOf<Int>()
|
||||||
|
val teamPlayers: Set<Player> 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.mapTo(playerIds) {
|
||||||
|
if (it != null && it is Number) it.toInt().also { id -> players.containsKey(id) }
|
||||||
|
else badRequest("invalid players array")
|
||||||
|
}
|
||||||
|
} ?: badRequest("missing players")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Serialization
|
// Serialization
|
||||||
|
|
||||||
fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament? = null) = Tournament(
|
fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = null): Tournament<*> {
|
||||||
id = json.getInt("id") ?: default?.id ?: Store.nextTournamentId,
|
val type = json.getString("type")?.uppercase()?.let { Tournament.Type.valueOf(it) } ?: default?.type ?: badRequest("missing type")
|
||||||
type = json.getString("type")?.uppercase()?.let { Tournament.Type.valueOf(it) } ?: default?.type ?: badRequest("missing type"),
|
// No clean way to avoid this redundancy
|
||||||
name = json.getString("name") ?: default?.name ?: badRequest("missing name"),
|
return if (type.playersNumber == 1)
|
||||||
shortName = json.getString("shortName") ?: default?.shortName ?: badRequest("missing shortName"),
|
StandardTournament(
|
||||||
startDate = json.getLocalDate("startDate") ?: default?.startDate ?: badRequest("missing startDate"),
|
id = json.getInt("id") ?: default?.id ?: Store.nextTournamentId,
|
||||||
endDate = json.getLocalDate("endDate") ?: default?.endDate ?: badRequest("missing endDate"),
|
type = type,
|
||||||
country = json.getString("country") ?: default?.country ?: badRequest("missing country"),
|
name = json.getString("name") ?: default?.name ?: badRequest("missing name"),
|
||||||
location = json.getString("location") ?: default?.location ?: badRequest("missing location"),
|
shortName = json.getString("shortName") ?: default?.shortName ?: badRequest("missing shortName"),
|
||||||
online = json.getBoolean("online") ?: default?.online ?: false,
|
startDate = json.getLocalDate("startDate") ?: default?.startDate ?: badRequest("missing startDate"),
|
||||||
komi = json.getDouble("komi") ?: default?.komi ?: 7.5,
|
endDate = json.getLocalDate("endDate") ?: default?.endDate ?: badRequest("missing endDate"),
|
||||||
rules = json.getString("rules")?.let { Rules.valueOf(it) } ?: default?.rules ?: Rules.FRENCH,
|
country = json.getString("country") ?: default?.country ?: badRequest("missing country"),
|
||||||
gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19,
|
location = json.getString("location") ?: default?.location ?: badRequest("missing location"),
|
||||||
timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"),
|
online = json.getBoolean("online") ?: default?.online ?: false,
|
||||||
rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"),
|
komi = json.getDouble("komi") ?: default?.komi ?: 7.5,
|
||||||
pairing = json.getObject("pairing")?.let { Pairing.fromJson(it) } ?: default?.pairing ?: badRequest("missing pairing")
|
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,
|
"id" to id,
|
||||||
"type" to type.name,
|
"type" to type.name,
|
||||||
"name" to name,
|
"name" to name,
|
||||||
|
@@ -29,7 +29,6 @@ class SwissSolver(history: List<Game>, pairables: List<Pairable>, weights: Pairi
|
|||||||
else abs(q.placeInGroup.first - (p.placeInGroup.second - p.placeInGroup.first)) * weights.place
|
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_RANDOM -> rand.nextDouble() * p.placeInGroup.second * weights.place
|
||||||
SPLIT_AND_SLIP -> abs(abs(p.placeInGroup.first - q.placeInGroup.first) - 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
|
} + (abs(p.colorBalance + 1) + abs(q.colorBalance - 1)) * weights.color
|
||||||
}
|
}
|
||||||
|
@@ -13,9 +13,9 @@ object Store {
|
|||||||
val nextPlayerId get() = _nextPlayerId.incrementAndGet()
|
val nextPlayerId get() = _nextPlayerId.incrementAndGet()
|
||||||
val nextGameId get() = _nextGameId.incrementAndGet()
|
val nextGameId get() = _nextGameId.incrementAndGet()
|
||||||
|
|
||||||
private val tournaments = mutableMapOf<Int, Tournament>()
|
private val tournaments = mutableMapOf<Int, Tournament<*>>()
|
||||||
|
|
||||||
fun addTournament(tournament: Tournament) {
|
fun addTournament(tournament: Tournament<*>) {
|
||||||
if (tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} already exists")
|
if (tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} already exists")
|
||||||
tournaments[tournament.id] = tournament
|
tournaments[tournament.id] = tournament
|
||||||
}
|
}
|
||||||
@@ -24,12 +24,12 @@ object Store {
|
|||||||
|
|
||||||
fun getTournamentsIDs(): Set<Int> = tournaments.keys
|
fun getTournamentsIDs(): Set<Int> = tournaments.keys
|
||||||
|
|
||||||
fun replaceTournament(tournament: Tournament) {
|
fun replaceTournament(tournament: Tournament<*>) {
|
||||||
if (!tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} not known")
|
if (!tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} not known")
|
||||||
tournaments[tournament.id] = tournament
|
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")
|
if (!tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} not known")
|
||||||
tournaments.remove(tournament.id)
|
tournaments.remove(tournament.id)
|
||||||
|
|
||||||
|
@@ -66,7 +66,7 @@ open class OptionalStringXmlDelegate(val xml: Element) {
|
|||||||
|
|
||||||
open class StringXmlDelegate(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 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) {
|
open class OptionalBooleanXmlDelegate(val xml: Element) {
|
||||||
@@ -76,7 +76,7 @@ open class OptionalBooleanXmlDelegate(val xml: Element) {
|
|||||||
|
|
||||||
open class BooleanXmlDelegate(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 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) {
|
open class OptionalIntXmlDelegate(val xml: Element) {
|
||||||
@@ -86,7 +86,7 @@ open class OptionalIntXmlDelegate(val xml: Element) {
|
|||||||
|
|
||||||
open class IntXmlDelegate(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)
|
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) {
|
open class OptionalLongXmlDelegate(val xml: Element) {
|
||||||
@@ -96,7 +96,7 @@ open class OptionalLongXmlDelegate(val xml: Element) {
|
|||||||
|
|
||||||
open class LongXmlDelegate(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)
|
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) {
|
open class OptionalDoubleXmlDelegate(val xml: Element) {
|
||||||
@@ -106,7 +106,7 @@ open class OptionalDoubleXmlDelegate(val xml: Element) {
|
|||||||
|
|
||||||
open class DoubleXmlDelegate(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)
|
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<E: Enum<*>> (val xml: Element, private val kclass: KClass<E>) {
|
open class OptionalEnumXmlDelegate<E: Enum<*>> (val xml: Element, private val kclass: KClass<E>) {
|
||||||
@@ -115,7 +115,7 @@ open class OptionalEnumXmlDelegate<E: Enum<*>> (val xml: Element, private val kc
|
|||||||
val xmlValue = xml.childOrNull(property.name)?.textContent
|
val xmlValue = xml.childOrNull(property.name)?.textContent
|
||||||
return enumValues.firstOrNull() { it.name == xmlValue }
|
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<E: Enum<*>> (val xml: Element, private val kclass: KClass<E>) {
|
open class EnumXmlDelegate<E: Enum<*>> (val xml: Element, private val kclass: KClass<E>) {
|
||||||
@@ -124,7 +124,7 @@ open class EnumXmlDelegate<E: Enum<*>> (val xml: Element, private val kclass: KC
|
|||||||
val xmlValue = xml.childOrNull(property.name)?.textContent
|
val xmlValue = xml.childOrNull(property.name)?.textContent
|
||||||
return enumValues.firstOrNull() { it.name == xmlValue } ?: error(property.name)
|
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"
|
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) {
|
open class OptionalDateXmlDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATE_FORMAT) {
|
||||||
private val format = dateTimeFormat(formatString)
|
private val format = dateTimeFormat(formatString)
|
||||||
open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDate? = xml.childOrNull(property.name)?.value()?.let { LocalDate.parse(it /*, format */) }
|
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) {
|
open class DateXmlDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATE_FORMAT) {
|
||||||
private val format = dateTimeFormat(formatString)
|
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)
|
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) {
|
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
|
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) {
|
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
|
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 <F: XmlFormat, T: Any> KClass<F>.instantiate(content: T): F = constructors.first().call(content)
|
fun <F: XmlFormat, T: Any> KClass<F>.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 class StringXmlAttrDelegate(val xml: Element) {
|
||||||
open operator fun getValue(thisRef: Any?, property: KProperty<*>): String = xml.attr(property.name) ?: error(property.name)
|
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) {
|
open class OptionalBooleanXmlAttrDelegate(val xml: Element) {
|
||||||
@@ -217,7 +217,7 @@ open class OptionalBooleanXmlAttrDelegate(val xml: Element) {
|
|||||||
|
|
||||||
open class BooleanXmlAttrDelegate(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 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) {
|
open class OptionalIntXmlAttrDelegate(val xml: Element) {
|
||||||
@@ -227,7 +227,7 @@ open class OptionalIntXmlAttrDelegate(val xml: Element) {
|
|||||||
|
|
||||||
open class IntXmlAttrDelegate(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 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) {
|
open class OptionalLongXmlAttrDelegate(val xml: Element) {
|
||||||
@@ -237,7 +237,7 @@ open class OptionalLongXmlAttrDelegate(val xml: Element) {
|
|||||||
|
|
||||||
open class LongXmlAttrDelegate(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 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) {
|
open class OptionalDoubleXmlAttrDelegate(val xml: Element) {
|
||||||
@@ -247,7 +247,7 @@ open class OptionalDoubleXmlAttrDelegate(val xml: Element) {
|
|||||||
|
|
||||||
open class DoubleXmlAttrDelegate(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 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) {
|
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) {
|
open class DateXmlAttrDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATE_FORMAT) {
|
||||||
private val format = dateTimeFormat(formatString)
|
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 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
|
// containers delegates
|
||||||
|
@@ -287,11 +287,9 @@ object XmlUtils {
|
|||||||
*/
|
*/
|
||||||
@Throws(XPathExpressionException::class)
|
@Throws(XPathExpressionException::class)
|
||||||
fun search(xpath: String?, context: Node?): NodeList {
|
fun search(xpath: String?, context: Node?): NodeList {
|
||||||
var ret: NodeList? = null
|
|
||||||
val xp = XPathFactory.newInstance().newXPath()
|
val xp = XPathFactory.newInstance().newXPath()
|
||||||
val exp = xp.compile(xpath)
|
val exp = xp.compile(xpath)
|
||||||
ret = exp.evaluate(context, XPathConstants.NODESET) as NodeList
|
return exp.evaluate(context, XPathConstants.NODESET) as NodeList
|
||||||
return ret
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -305,7 +303,7 @@ object XmlUtils {
|
|||||||
fun getNodes(xpath: String?, context: Node?): List<Node> {
|
fun getNodes(xpath: String?, context: Node?): List<Node> {
|
||||||
val ret: MutableList<Node> = ArrayList()
|
val ret: MutableList<Node> = ArrayList()
|
||||||
val lst = search(xpath, context)
|
val lst = search(xpath, context)
|
||||||
for (i in 0 until lst!!.length) {
|
for (i in 0 until lst.length) {
|
||||||
ret.add(lst.item(i))
|
ret.add(lst.item(i))
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
@@ -322,7 +320,7 @@ object XmlUtils {
|
|||||||
fun getElements(xpath: String?, context: Node?): List<Element> {
|
fun getElements(xpath: String?, context: Node?): List<Element> {
|
||||||
val ret: MutableList<Element> = ArrayList()
|
val ret: MutableList<Element> = ArrayList()
|
||||||
val lst = search(xpath, context)
|
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,
|
// will throw a ClassCastExpression if Node is not an Element,
|
||||||
// that's what we want
|
// that's what we want
|
||||||
ret.add(lst.item(i) as Element)
|
ret.add(lst.item(i) as Element)
|
||||||
@@ -340,7 +338,7 @@ object XmlUtils {
|
|||||||
fun nodePath(n: Node): String {
|
fun nodePath(n: Node): String {
|
||||||
|
|
||||||
// declarations
|
// declarations
|
||||||
var parent: Node? = null
|
var parent: Node?
|
||||||
val hierarchy = Stack<Node>()
|
val hierarchy = Stack<Node>()
|
||||||
val buffer = StringBuffer("/")
|
val buffer = StringBuffer("/")
|
||||||
|
|
||||||
@@ -528,7 +526,7 @@ fun Node.trimTextNodes() {
|
|||||||
val children: NodeList = getChildNodes()
|
val children: NodeList = getChildNodes()
|
||||||
for (i in 0 until children.length) {
|
for (i in 0 until children.length) {
|
||||||
val child = children.item(i)
|
val child = children.item(i)
|
||||||
if (child.nodeType === Node.TEXT_NODE) {
|
if (child.nodeType == Node.TEXT_NODE) {
|
||||||
child.textContent = child.textContent.trim()
|
child.textContent = child.textContent.trim()
|
||||||
}
|
}
|
||||||
else child.trimTextNodes()
|
else child.trimTextNodes()
|
||||||
|
@@ -5,6 +5,7 @@ import org.jeudego.pairgoth.api.ApiHandler
|
|||||||
import org.jeudego.pairgoth.api.PairingHandler
|
import org.jeudego.pairgoth.api.PairingHandler
|
||||||
import org.jeudego.pairgoth.api.PlayerHandler
|
import org.jeudego.pairgoth.api.PlayerHandler
|
||||||
import org.jeudego.pairgoth.api.ResultsHandler
|
import org.jeudego.pairgoth.api.ResultsHandler
|
||||||
|
import org.jeudego.pairgoth.api.TeamHandler
|
||||||
import org.jeudego.pairgoth.api.TournamentHandler
|
import org.jeudego.pairgoth.api.TournamentHandler
|
||||||
import org.jeudego.pairgoth.util.Colorizer.blue
|
import org.jeudego.pairgoth.util.Colorizer.blue
|
||||||
import org.jeudego.pairgoth.util.Colorizer.green
|
import org.jeudego.pairgoth.util.Colorizer.green
|
||||||
@@ -86,6 +87,7 @@ class ApiServlet : HttpServlet() {
|
|||||||
"part" -> PlayerHandler
|
"part" -> PlayerHandler
|
||||||
"pair" -> PairingHandler
|
"pair" -> PairingHandler
|
||||||
"res" -> ResultsHandler
|
"res" -> ResultsHandler
|
||||||
|
"team" -> TeamHandler
|
||||||
else -> ApiHandler.badRequest("unknown sub-entity: $subEntity")
|
else -> ApiHandler.badRequest("unknown sub-entity: $subEntity")
|
||||||
}
|
}
|
||||||
"player" -> PlayerHandler
|
"player" -> PlayerHandler
|
||||||
@@ -103,7 +105,7 @@ class ApiServlet : HttpServlet() {
|
|||||||
|
|
||||||
} catch (apiException: ApiException) {
|
} catch (apiException: ApiException) {
|
||||||
reason = apiException.message ?: "unknown API error"
|
reason = apiException.message ?: "unknown API error"
|
||||||
if (reason == null) error(response, apiException.code) else error(
|
error(
|
||||||
request,
|
request,
|
||||||
response,
|
response,
|
||||||
apiException.code,
|
apiException.code,
|
||||||
@@ -190,7 +192,7 @@ class ApiServlet : HttpServlet() {
|
|||||||
// some API calls like opengotha import accept xml docs as body
|
// some API calls like opengotha import accept xml docs as body
|
||||||
// CB TODO - limit to those calls
|
// CB TODO - limit to those calls
|
||||||
try {
|
try {
|
||||||
XmlUtils.parse(request.reader)?.let { payload: Element ->
|
XmlUtils.parse(request.reader).let { payload: Element ->
|
||||||
request.setAttribute(ApiHandler.PAYLOAD_KEY, payload)
|
request.setAttribute(ApiHandler.PAYLOAD_KEY, payload)
|
||||||
logger.info(blue("<< (xml document)"))
|
logger.info(blue("<< (xml document)"))
|
||||||
|
|
||||||
|
@@ -10,6 +10,9 @@ enum class Event {
|
|||||||
playerAdded,
|
playerAdded,
|
||||||
playerUpdated,
|
playerUpdated,
|
||||||
playerDeleted,
|
playerDeleted,
|
||||||
|
teamAdded,
|
||||||
|
teamUpdated,
|
||||||
|
teamDeleted,
|
||||||
gamesAdded,
|
gamesAdded,
|
||||||
gamesDeleted,
|
gamesDeleted,
|
||||||
resultUpdated,
|
resultUpdated,
|
||||||
|
@@ -104,9 +104,6 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H
|
|||||||
override fun contextDestroyed(sce: ServletContextEvent) {
|
override fun contextDestroyed(sce: ServletContextEvent) {
|
||||||
logger.info("---------- Stopping Web Application ----------")
|
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
|
val context = sce.servletContext
|
||||||
for (service in webServices.keys) stopService(service, true)
|
for (service in webServices.keys) stopService(service, true)
|
||||||
// ??? DriverManager.deregisterDriver(com.mysql.cj.jdbc.Driver ...);
|
// ??? DriverManager.deregisterDriver(com.mysql.cj.jdbc.Driver ...);
|
||||||
|
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.MethodOrderer.Alphanumeric
|
|||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.TestInstance
|
import org.junit.jupiter.api.TestInstance
|
||||||
import org.junit.jupiter.api.TestMethodOrder
|
import org.junit.jupiter.api.TestMethodOrder
|
||||||
|
import java.util.Objects
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertTrue
|
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(
|
val aPlayer = Json.Object(
|
||||||
"name" to "Burma",
|
"name" to "Burma",
|
||||||
"firstname" to "Nestor",
|
"firstname" to "Nestor",
|
||||||
@@ -98,4 +120,27 @@ class BasicTests: TestBase() {
|
|||||||
"[{\"id\":1,\"w\":2,\"b\":1,\"r\":\"?\"}]")
|
"[{\"id\":1,\"w\":2,\"b\":1,\"r\":\"?\"}]")
|
||||||
assertTrue(possibleResults.contains(games.toString()), "pairing differs")
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user