Opengotha export and basic test

This commit is contained in:
Claude Brisson
2023-05-20 18:01:07 +02:00
parent 2858ce4186
commit 03fa9d68c1
9 changed files with 146 additions and 21 deletions

View File

@@ -9,15 +9,16 @@ import javax.servlet.http.HttpServletResponse
interface ApiHandler {
fun route(request: HttpServletRequest, response: HttpServletResponse) =
// for now, only get() needed the response object ; other methods shall be reengineered as well if needed
when (request.method) {
"GET" -> get(request)
"GET" -> get(request, response)
"POST" -> post(request)
"PUT" -> put(request)
"DELETE" -> delete(request)
else -> notImplemented()
}
fun get(request: HttpServletRequest): Json {
fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
notImplemented()
}

View File

@@ -8,10 +8,11 @@ import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.web.Event
import org.jeudego.pairgoth.web.Event.*
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
object PairingHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest): Json {
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
val playing = (tournament.games.getOrNull(round)?.values ?: emptyList()).flatMap {

View File

@@ -8,10 +8,11 @@ import org.jeudego.pairgoth.model.fromJson
import org.jeudego.pairgoth.web.Event
import org.jeudego.pairgoth.web.Event.*
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
object PlayerHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest): Json {
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request) ?: badRequest("invalid tournament")
return when (val pid = getSubSelector(request)?.toIntOrNull()) {
null -> tournament.pairables.values.map { it.toJson() }.toJsonArray()

View File

@@ -4,15 +4,14 @@ import com.republicate.kson.Json
import com.republicate.kson.toJsonArray
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.store.Store
import org.jeudego.pairgoth.web.Event
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
object ResultsHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest): Json {
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
val games = tournament.games.getOrNull(round)?.values ?: emptyList()

View File

@@ -8,17 +8,30 @@ import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.fromJson
import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.store.Store
import org.jeudego.pairgoth.web.ApiServlet
import org.jeudego.pairgoth.web.Event
import org.jeudego.pairgoth.web.Event.*
import org.w3c.dom.Element
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
object TournamentHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest): Json {
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
val accept = request.getHeader("Accept")
return when (val id = getSelector(request)?.toIntOrNull()) {
null -> Json.Array(Store.getTournamentsIDs())
else -> Store.getTournament(id)?.toJson() ?: badRequest("no tournament with id #${id}")
else ->
when {
ApiServlet.isJson(accept) -> Store.getTournament(id)?.toJson() ?: badRequest("no tournament with id #${id}")
ApiServlet.isXml(accept) -> {
val export = Store.getTournament(id)?.let { OpenGotha.export(it) } ?: badRequest("no tournament with id #${id}")
response.contentType = "application/xml; charset=UTF-8"
response.writer.write(export)
null // return null to indicate that we handled the response ourself
}
else -> badRequest("unhandled Accept header: $accept")
}
}
}

View File

@@ -9,7 +9,9 @@ import org.jeudego.pairgoth.model.Player
import org.jeudego.pairgoth.model.StandardByoyomi
import org.jeudego.pairgoth.model.SuddenDeath
import org.jeudego.pairgoth.model.Swiss
import org.jeudego.pairgoth.model.TimeSystem
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.displayRank
import org.jeudego.pairgoth.model.parseRank
import org.jeudego.pairgoth.store.Store
import org.jeudego.pairgoth.util.XmlFormat
@@ -173,4 +175,110 @@ object OpenGotha {
tournament.games.addAll(gamesPerRound)
return tournament
}
// TODO - bye player(s)
fun export(tournament: Tournament): String {
val xml = """
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Tournament dataVersion="201" externalIPAddress="88.122.144.219" fullVersionNumber="3.51" runningMode="SAL" saveDT="20210111180800">
<Players>
${tournament.pairables.values.map { player ->
player as Player
}.joinToString("\n") { player ->
"""<Player agaExpirationDate="" agaId="" club="${
player.club
}" country="${
player.country
}" egfPin="" ffgLicence="" ffgLicenceStatus="" firstName="${
player.firstname
}" grade="${
player.displayRank()
}" name="${
player.name
}" participating="${
(1..20).map {
if (player.skip.contains(it)) 0 else 1
}.joinToString("")
}" rank="${
player.displayRank()
}" rating="${
player.rating
}" ratingOrigin="" registeringStatus="FIN" smmsCorrection="0"/>"""
}
}
</Players>
<Games>
${tournament.games.flatMapIndexed { round, games ->
games.values.mapIndexed { table, game ->
Triple(round, table , game)
}
}.joinToString("\n") { (round, table, game) ->
"""<Game blackPlayer="${
(tournament.pairables[game.black]!! as Player).let { black ->
"${black.name}${black.firstname}".uppercase(Locale.ENGLISH) // Use Locale.ENGLISH to transform é to É
}
}" handicap="0" knownColor="true" result="${
when (game.result) {
Game.Result.UNKNOWN, Game.Result.CANCELLED -> "RESULT_UNKNOWN"
Game.Result.BLACK -> "RESULT_BLACKWINS"
Game.Result.WHITE -> "RESULT_WHITEWINS"
Game.Result.JIGO -> "RESULT_EQUAL"
Game.Result.BOTHWIN -> "RESULT_BOTHWIN"
Game.Result.BOTHLOOSE -> "RESULT_BOTHLOOSE"
else -> throw Error("unhandled game result")
}
}" roundNumber="${
round + 1
}" tableNumber="${
table + 1
}" whitePlayer="${
(tournament.pairables[game.white]!! as Player).let { white ->
"${white.name}${white.firstname}".uppercase(Locale.ENGLISH) // Use Locale.ENGLISH to transform é to É
}
}"/>"""
}
}
</Games>
<ByePlayer>
</ByePlayer>
<TournamentParameterSet>
<GeneralParameterSet bInternet="${tournament.online}" basicTime="${tournament.timeSystem.mainTime}" beginDate="${tournament.startDate}" canByoYomiTime="${tournament.timeSystem.byoyomi}" complementaryTimeSystem="${when(tournament.timeSystem.type) {
TimeSystem.TimeSystemType.SUDDEN_DEATH -> "SUDDENDEATH"
TimeSystem.TimeSystemType.STANDARD -> "STDBYOYOMI"
TimeSystem.TimeSystemType.CANADIAN -> "CANBYOYOMI"
TimeSystem.TimeSystemType.FISCHER -> "FISCHER"
} }" director="" endDate="${tournament.endDate}" fischerTime="${tournament.timeSystem.increment}" genCountNotPlayedGamesAsHalfPoint="false" genMMBar="9D" genMMFloor="30K" genMMS2ValueAbsent="1" genMMS2ValueBye="2" genMMZero="30K" genNBW2ValueAbsent="0" genNBW2ValueBye="2" genRoundDownNBWMMS="true" komi="${tournament.komi}" location="${tournament.location}" name="${tournament.name}" nbMovesCanTime="${tournament.timeSystem.stones}" numberOfCategories="1" numberOfRounds="${tournament.rounds}" shortName="${tournament.shortName}" size="${tournament.gobanSize}" stdByoYomiTime="${tournament.timeSystem.byoyomi}"/>
<HandicapParameterSet hdBasedOnMMS="false" hdCeiling="0" hdCorrection="0" hdNoHdRankThreshold="30K"/>
<PlacementParameterSet>
<PlacementCriteria>
<PlacementCriterion name="NBW" number="1"/>
<PlacementCriterion name="SOSW" number="2"/>
<PlacementCriterion name="SOSOSW" number="3"/>
<PlacementCriterion name="NULL" number="4"/>
<PlacementCriterion name="NULL" number="5"/>
<PlacementCriterion name="NULL" number="6"/>
</PlacementCriteria>
</PlacementParameterSet>
<PairingParameterSet paiBaAvoidDuplGame="500000000000000" paiBaBalanceWB="1000000" paiBaDeterministic="true" paiBaRandom="0" paiMaAdditionalPlacementCritSystem1="Rating" paiMaAdditionalPlacementCritSystem2="Rating" paiMaAvoidMixingCategories="0" paiMaCompensateDUDD="true" paiMaDUDDLowerMode="MID" paiMaDUDDUpperMode="MID" paiMaDUDDWeight="100000000" paiMaLastRoundForSeedSystem1="2" paiMaMaximizeSeeding="5000000" paiMaMinimizeScoreDifference="100000000000" paiMaSeedSystem1="SPLITANDSLIP" paiMaSeedSystem2="SPLITANDSLIP" paiSeAvoidSameGeo="0" paiSeBarThresholdActive="true" paiSeDefSecCrit="20000000000000" paiSeMinimizeHandicap="0" paiSeNbWinsThresholdActive="true" paiSePreferMMSDiffRatherThanSameClub="0" paiSePreferMMSDiffRatherThanSameCountry="0" paiSeRankThreshold="30K" paiStandardNX1Factor="0.5"/>
<DPParameterSet displayClCol="true" displayCoCol="true" displayIndGamesInMatches="true" displayNPPlayers="false" displayNumCol="true" displayPlCol="true" gameFormat="short" playerSortType="name" showByePlayer="true" showNotFinallyRegisteredPlayers="true" showNotPairedPlayers="true" showNotParticipatingPlayers="false" showPlayerClub="true" showPlayerCountry="false" showPlayerGrade="true"/>
<PublishParameterSet exportToLocalFile="true" htmlAutoScroll="false" print="false"/>
</TournamentParameterSet>
<TeamTournamentParameterSet>
<TeamGeneralParameterSet teamSize="4"/>
<TeamPlacementParameterSet>
<PlacementCriteria>
<PlacementCriterion name="TEAMP" number="1"/>
<PlacementCriterion name="BDW" number="2"/>
<PlacementCriterion name="BDW3U" number="3"/>
<PlacementCriterion name="BDW2U" number="4"/>
<PlacementCriterion name="BDW1U" number="5"/>
<PlacementCriterion name="MNR" number="6"/>
</PlacementCriteria>
</TeamPlacementParameterSet>
</TeamTournamentParameterSet>
</Tournament>
""".trimIndent()
return xml
}
}

View File

@@ -213,7 +213,7 @@ class ApiServlet : HttpServlet() {
HttpServletResponse.SC_BAD_REQUEST,
"Missing 'Accept' header"
)
if (!isJson(accept)) throw ApiException(
if (!isJson(accept) && (!isXml(accept) || !request.requestURI.matches(Regex("/api/tour/\\d+")))) throw ApiException(
HttpServletResponse.SC_BAD_REQUEST,
"Invalid 'Accept' header"
)
@@ -260,7 +260,7 @@ class ApiServlet : HttpServlet() {
private const val EXPECTED_CHARSET = "utf8"
const val AUTH_HEADER = "Authorization"
const val AUTH_PREFIX = "Bearer"
private fun isJson(mimeType: String) = "text/json" == mimeType || "application/json" == mimeType || mimeType.endsWith("+json")
private fun isXml(mimeType: String) = "text/xml" == mimeType || "application/xml" == mimeType || mimeType.endsWith("+xml")
fun isJson(mimeType: String) = "text/json" == mimeType || "application/json" == mimeType || mimeType.endsWith("+json")
fun isXml(mimeType: String) = "text/xml" == mimeType || "application/xml" == mimeType || mimeType.endsWith("+xml")
}
}

View File

@@ -1,10 +1,9 @@
package org.jeudego.pairgoth.test
import org.junit.jupiter.api.Test
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
class ImportTests: TestBase() {
class ImportExportTests: TestBase() {
@Test
fun `001 test imports`() {
@@ -21,6 +20,8 @@ class ImportTests: TestBase() {
val games = TestAPI.get("/api/tour/$id/res/1").asArray()
logger.info("games for round $round: {}", games.toString())
}
val xml = TestAPI.getXml("/api/tour/$id")
logger.info(xml)
}
}
}

View File

@@ -34,7 +34,7 @@ object TestAPI {
private val apiServlet = ApiServlet()
private val sseServlet = SSEServlet()
private fun <T> testRequest(reqMethod: String, uri: String, payload: T? = null): Json {
private fun <T> testRequest(reqMethod: String, uri: String, accept: String = "application/json", payload: T? = null): String {
WebappManager.properties["webapp.env"] = "test"
@@ -64,7 +64,7 @@ object TestAPI {
else -> throw Error("unhandled case")
}
on { headerNames } doReturn Collections.enumeration(myHeaderNames)
on { getHeader(eq("Accept")) } doReturn "application/json"
on { getHeader(eq("Accept")) } doReturn accept
}
// mock response
@@ -83,13 +83,14 @@ object TestAPI {
"DELETE" -> apiServlet.doDelete(req, resp)
}
return Json.parse(buffer.toString()) ?: throw Error("no response payload")
return buffer.toString() ?: throw Error("no response payload")
}
fun get(uri: String) = testRequest<Void>("GET", uri)
fun <T> post(uri: String, payload: T) = testRequest("POST", uri, payload)
fun <T> put(uri: String, payload: T) = testRequest("PUT", uri, payload)
fun <T> delete(uri: String, payload: T) = testRequest("DELETE", uri, payload)
fun get(uri: String): Json = Json.parse(testRequest<Void>("GET", uri)) ?: throw Error("no payload")
fun getXml(uri: String): String = testRequest<Void>("GET", uri, "application/xml")
fun <T> post(uri: String, payload: T) = Json.parse(testRequest("POST", uri, payload = payload)) ?: throw Error("no payload")
fun <T> put(uri: String, payload: T) = Json.parse(testRequest("PUT", uri, payload = payload)) ?: throw Error("no payload")
fun <T> delete(uri: String, payload: T) = Json.parse(testRequest("DELETE", uri, payload = payload)) ?: throw Error("no payload")
}
// Get a list of resources