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 { interface ApiHandler {
fun route(request: HttpServletRequest, response: HttpServletResponse) = 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) { when (request.method) {
"GET" -> get(request) "GET" -> get(request, response)
"POST" -> post(request) "POST" -> post(request)
"PUT" -> put(request) "PUT" -> put(request)
"DELETE" -> delete(request) "DELETE" -> delete(request)
else -> notImplemented() else -> notImplemented()
} }
fun get(request: HttpServletRequest): Json { fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
notImplemented() 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 org.jeudego.pairgoth.web.Event.* import org.jeudego.pairgoth.web.Event.*
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
object PairingHandler: PairgothApiHandler { object PairingHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest): Json { override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request) val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number") val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
val playing = (tournament.games.getOrNull(round)?.values ?: emptyList()).flatMap { 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 org.jeudego.pairgoth.web.Event.* import org.jeudego.pairgoth.web.Event.*
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
object PlayerHandler: PairgothApiHandler { object PlayerHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest): Json { override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request) ?: badRequest("invalid tournament") val tournament = getTournament(request) ?: badRequest("invalid tournament")
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()

View File

@@ -4,15 +4,14 @@ import com.republicate.kson.Json
import com.republicate.kson.toJsonArray import com.republicate.kson.toJsonArray
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.Game import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.toJson import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.store.Store
import org.jeudego.pairgoth.web.Event import org.jeudego.pairgoth.web.Event
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
object ResultsHandler: PairgothApiHandler { object ResultsHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest): Json { override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request) val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number") val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
val games = tournament.games.getOrNull(round)?.values ?: emptyList() 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.fromJson
import org.jeudego.pairgoth.model.toJson import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.store.Store 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.jeudego.pairgoth.web.Event.* import org.jeudego.pairgoth.web.Event.*
import org.w3c.dom.Element import org.w3c.dom.Element
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
object TournamentHandler: PairgothApiHandler { 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()) { return when (val id = getSelector(request)?.toIntOrNull()) {
null -> Json.Array(Store.getTournamentsIDs()) 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.StandardByoyomi
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.Tournament import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.displayRank
import org.jeudego.pairgoth.model.parseRank import org.jeudego.pairgoth.model.parseRank
import org.jeudego.pairgoth.store.Store import org.jeudego.pairgoth.store.Store
import org.jeudego.pairgoth.util.XmlFormat import org.jeudego.pairgoth.util.XmlFormat
@@ -173,4 +175,110 @@ object OpenGotha {
tournament.games.addAll(gamesPerRound) tournament.games.addAll(gamesPerRound)
return tournament 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, HttpServletResponse.SC_BAD_REQUEST,
"Missing 'Accept' header" "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, HttpServletResponse.SC_BAD_REQUEST,
"Invalid 'Accept' header" "Invalid 'Accept' header"
) )
@@ -260,7 +260,7 @@ class ApiServlet : HttpServlet() {
private const val EXPECTED_CHARSET = "utf8" private const val EXPECTED_CHARSET = "utf8"
const val AUTH_HEADER = "Authorization" const val AUTH_HEADER = "Authorization"
const val AUTH_PREFIX = "Bearer" const val AUTH_PREFIX = "Bearer"
private fun isJson(mimeType: String) = "text/json" == mimeType || "application/json" == mimeType || mimeType.endsWith("+json") 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 isXml(mimeType: String) = "text/xml" == mimeType || "application/xml" == mimeType || mimeType.endsWith("+xml")
} }
} }

View File

@@ -1,10 +1,9 @@
package org.jeudego.pairgoth.test package org.jeudego.pairgoth.test
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
class ImportTests: TestBase() { class ImportExportTests: TestBase() {
@Test @Test
fun `001 test imports`() { fun `001 test imports`() {
@@ -21,6 +20,8 @@ class ImportTests: TestBase() {
val games = TestAPI.get("/api/tour/$id/res/1").asArray() val games = TestAPI.get("/api/tour/$id/res/1").asArray()
logger.info("games for round $round: {}", games.toString()) 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 apiServlet = ApiServlet()
private val sseServlet = SSEServlet() 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" WebappManager.properties["webapp.env"] = "test"
@@ -64,7 +64,7 @@ object TestAPI {
else -> throw Error("unhandled case") else -> throw Error("unhandled case")
} }
on { headerNames } doReturn Collections.enumeration(myHeaderNames) on { headerNames } doReturn Collections.enumeration(myHeaderNames)
on { getHeader(eq("Accept")) } doReturn "application/json" on { getHeader(eq("Accept")) } doReturn accept
} }
// mock response // mock response
@@ -83,13 +83,14 @@ object TestAPI {
"DELETE" -> apiServlet.doDelete(req, resp) "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 get(uri: String): Json = Json.parse(testRequest<Void>("GET", uri)) ?: throw Error("no payload")
fun <T> post(uri: String, payload: T) = testRequest("POST", uri, payload) fun getXml(uri: String): String = testRequest<Void>("GET", uri, "application/xml")
fun <T> put(uri: String, payload: T) = testRequest("PUT", uri, payload) fun <T> post(uri: String, payload: T) = Json.parse(testRequest("POST", uri, payload = payload)) ?: throw Error("no payload")
fun <T> delete(uri: String, payload: T) = testRequest("DELETE", uri, 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 // Get a list of resources