Reenginering to prepare view webapp

This commit is contained in:
Claude Brisson
2023-06-07 09:34:44 +02:00
parent 231b7d68dd
commit 478c1e1f9d
64 changed files with 48 additions and 24 deletions

View File

@@ -0,0 +1,5 @@
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure id="wac" class="org.eclipse.jetty.webapp.WebAppContext">
</Configure>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Call name="addConnector">
<Arg>
<New class="org.eclipse.jetty.server.ServerConnector">
<Arg name="server">
<Ref refid="Server"/>
</Arg>
<Arg name="factories">
<Array type="org.eclipse.jetty.server.ConnectionFactory">
<Item>
<New class="org.eclipse.jetty.server.HttpConnectionFactory">
<Arg name="config">
<New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
</New>
</Arg>
</New>
</Item>
</Array>
</Arg>
<Set name="port"><Property name="jetty.http.port" deprecated="jetty.port" default="${jetty.port}"/></Set>
</New>
</Arg>
</Call>
</Configure>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<!-- ============================================================= --><!-- Configure an HTTPS connector. --><!-- This configuration must be used in conjunction with jetty.xml --><!-- and jetty-ssl.xml. --><!-- ============================================================= -->
<Configure id="sslConnector" class="org.eclipse.jetty.server.ServerConnector">
<Call name="addIfAbsentConnectionFactory">
<Arg>
<New class="org.eclipse.jetty.server.SslConnectionFactory">
<Arg name="next">http/1.1</Arg>
<Arg name="sslContextFactory"><Ref refid="sslContextFactory"/></Arg>
</New>
</Arg>
</Call>
<Call name="addConnectionFactory">
<Arg>
<New class="org.eclipse.jetty.server.HttpConnectionFactory">
<Arg name="config"><Ref refid="sslHttpConfig" /></Arg>
<Arg name="compliance"><Call class="org.eclipse.jetty.http.HttpCompliance" name="valueOf"><Arg><Property name="jetty.http.compliance" default="RFC7230"/></Arg></Call></Arg>
</New>
</Arg>
</Call>
</Configure>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory$Server">
<Set name="TrustAll">true</Set>
<Set name="KeyStorePath"><Property name="jetty.base" default="." />/<Property name="jetty.keystore" default="src/test/resources/jetty.keystore"/></Set>
<Set name="KeyStorePassword"><Property name="jetty.keystore.password" default="secret"/></Set>
<Set name="KeyManagerPassword"><Property name="jetty.keymanager.password" default="secret"/></Set>
<Set name="TrustStorePath"><Property name="jetty.base" default="." />/<Property name="jetty.truststore" default="src/test/resources/jetty.keystore"/></Set>
<Set name="TrustStorePassword"><Property name="jetty.truststore.password" default="secret"/></Set>
<Set name="EndpointIdentificationAlgorithm"></Set>
<New id="sslHttpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
<Arg><Ref refid="httpConfig"/></Arg>
<Call name="addCustomizer">
<Arg><New class="org.eclipse.jetty.server.SecureRequestCustomizer"/></Arg>
</Call>
</New>
</Configure>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Call name="addConnector">
<Arg>
<New id="sslConnector" class="org.eclipse.jetty.server.ServerConnector">
<Arg name="server"><Ref refid="Server" /></Arg>
<Arg name="acceptors" type="int"><Property name="jetty.ssl.acceptors" deprecated="ssl.acceptors" default="-1"/></Arg>
<Arg name="selectors" type="int"><Property name="jetty.ssl.selectors" deprecated="ssl.selectors" default="-1"/></Arg>
<Arg name="factories">
<Array type="org.eclipse.jetty.server.ConnectionFactory">
</Array>
</Arg>
<Set name="host"><Property name="jetty.ssl.host" deprecated="jetty.host" /></Set>
<Set name="port"><Property name="jetty.ssl.port" deprecated="ssl.port" default="${jetty.ssl.port}" /></Set>
<Set name="idleTimeout"><Property name="jetty.ssl.idleTimeout" deprecated="ssl.timeout" default="30000"/></Set>
<Set name="acceptorPriorityDelta"><Property name="jetty.ssl.acceptorPriorityDelta" deprecated="ssl.acceptorPriorityDelta" default="0"/></Set>
<Set name="acceptQueueSize"><Property name="jetty.ssl.acceptQueueSize" deprecated="ssl.acceptQueueSize" default="0"/></Set>
<Set name="reuseAddress"><Property name="jetty.ssl.reuseAddress" default="true"/></Set>
<Set name="acceptedTcpNoDelay"><Property name="jetty.ssl.acceptedTcpNoDelay" default="true"/></Set>
<Set name="acceptedReceiveBufferSize"><Property name="jetty.ssl.acceptedReceiveBufferSize" default="-1"/></Set>
<Set name="acceptedSendBufferSize"><Property name="jetty.ssl.acceptedSendBufferSize" default="-1"/></Set>
<Get name="SelectorManager">
<Set name="connectTimeout"><Property name="jetty.ssl.connectTimeout" default="15000"/></Set>
</Get>
</New>
</Arg>
</Call>
<New id="sslHttpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
<Arg><Ref refid="httpConfig"/></Arg>
<Call name="addCustomizer">
<Arg>
<New class="org.eclipse.jetty.server.SecureRequestCustomizer">
<Arg name="sniRequired" type="boolean"><Property name="jetty.ssl.sniRequired" default="false"/></Arg>
<Arg name="sniHostCheck" type="boolean"><Property name="jetty.ssl.sniHostCheck" default="true"/></Arg>
<Arg name="stsMaxAgeSeconds" type="int"><Property name="jetty.ssl.stsMaxAgeSeconds" default="-1"/></Arg>
<Arg name="stsIncludeSubdomains" type="boolean"><Property name="jetty.ssl.stsIncludeSubdomains" default="false"/></Arg>
</New>
</Arg>
</Call>
</New>
</Configure>

View File

@@ -0,0 +1,8 @@
# webapp
webapp.env = ${webapp.env}
webapp.url = ${webapp.url}
# smtp
# Logging
logger.level = ${logger.level}

View File

@@ -0,0 +1,72 @@
package org.jeudego.pairgoth.api
import com.republicate.kson.Json
import org.jeudego.pairgoth.web.ApiException
import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServletRequest
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, response)
"POST" -> post(request)
"PUT" -> put(request)
"DELETE" -> delete(request)
else -> notImplemented()
}
fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
notImplemented()
}
fun post(request: HttpServletRequest): Json {
notImplemented()
}
fun put(request: HttpServletRequest): Json {
notImplemented()
}
fun delete(request: HttpServletRequest): Json {
notImplemented()
}
fun notImplemented(): Nothing {
throw ApiException(HttpServletResponse.SC_BAD_REQUEST)
}
fun getPayload(request: HttpServletRequest): Json {
return request.getAttribute(PAYLOAD_KEY) as Json? ?: throw ApiException(HttpServletResponse.SC_BAD_REQUEST, "no payload")
}
fun getObjectPayload(request: HttpServletRequest): Json.Object {
val json = getPayload(request)
if (!json.isObject) badRequest("expecting a json object")
return json.asObject()
}
fun getArrayPayload(request: HttpServletRequest): Json.Array {
val json = getPayload(request)
if (!json.isArray) badRequest("expecting a json array")
return json.asArray()
}
fun getSelector(request: HttpServletRequest): String? {
return request.getAttribute(SELECTOR_KEY) as String?
}
fun getSubSelector(request: HttpServletRequest): String? {
return request.getAttribute(SUBSELECTOR_KEY) as String?
}
companion object {
const val PAYLOAD_KEY = "PAYLOAD"
const val SELECTOR_KEY = "SELECTOR"
const val SUBSELECTOR_KEY = "SUBSELECTOR"
val logger = LoggerFactory.getLogger("api")
fun badRequest(msg: String = "bad request"): Nothing = throw ApiException(HttpServletResponse.SC_BAD_REQUEST, msg)
}
}

View File

@@ -0,0 +1,23 @@
package org.jeudego.pairgoth.api
import com.republicate.kson.Json
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.store.Store
import org.jeudego.pairgoth.web.Event
import javax.servlet.http.HttpServletRequest
interface PairgothApiHandler: ApiHandler {
fun getTournament(request: HttpServletRequest): Tournament<*> {
val tournamentId = getSelector(request)?.toIntOrNull() ?: ApiHandler.badRequest("invalid tournament id")
return Store.getTournament(tournamentId) ?: ApiHandler.badRequest("unknown tournament id")
}
fun Tournament<*>.dispatchEvent(event: Event, data: Json? = null) {
Event.dispatch(event, Json.Object("tournament" to id, "data" to data))
// when storage is not in memory, the tournament has to be persisted
if (event != Event.tournamentAdded && event != Event.tournamentDeleted && event != Event.gameUpdated)
Store.replaceTournament(this)
}
}

View File

@@ -0,0 +1,85 @@
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.Pairing
import org.jeudego.pairgoth.model.getID
import org.jeudego.pairgoth.model.toID
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, response: HttpServletResponse): Json? {
val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
val playing = tournament.games(round).values.flatMap {
listOf(it.black, it.white)
}.toSet()
return tournament.pairables.values.filter { !it.skip.contains(round) && !playing.contains(it.id) }.map { it.id }.toJsonArray()
}
override fun post(request: HttpServletRequest): Json {
val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
val payload = getArrayPayload(request)
val allPlayers = payload.size == 1 && payload[0] == "all"
if (!allPlayers && tournament.pairing.type == Pairing.PairingType.SWISS) badRequest("Swiss pairing requires all pairable players")
val playing = (tournament.games(round).values).flatMap {
listOf(it.black, it.white)
}.toSet()
val pairables =
if (allPlayers)
tournament.pairables.values.filter { !it.skip.contains(round) && !playing.contains(it.id) }
else payload.map {
// CB - because of the '["all"]' map, conversion to int lands here... Better API syntax for 'all players'?
if (it is Number) it.toID() else badRequest("invalid pairable id: #$it")
}.map { id ->
tournament.pairables[id]?.also {
if (it.skip.contains(round)) badRequest("pairable #$id does not play round $round")
if (playing.contains(it.id)) badRequest("pairable #$id already plays round $round")
} ?: badRequest("invalid pairable id: #$id")
}
val games = tournament.pair(round, pairables)
val ret = games.map { it.toJson() }.toJsonArray()
tournament.dispatchEvent(gamesAdded, Json.Object("round" to round, "games" to ret))
return ret
}
override fun put(request: HttpServletRequest): Json {
val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
// only allow last round (if players have not been paired in the last round, it *may* be possible to be more laxist...)
if (round != tournament.lastRound()) badRequest("cannot edit pairings in other rounds but the last")
val payload = getObjectPayload(request)
val game = tournament.games(round)[payload.getInt("id")] ?: badRequest("invalid game id")
game.black = payload.getID("b") ?: badRequest("missing black player id")
game.white = payload.getID("w") ?: badRequest("missing white player id")
if (payload.containsKey("h")) game.handicap = payload.getString("h")?.toIntOrNull() ?: badRequest("invalid handicap")
tournament.dispatchEvent(gameUpdated, Json.Object("round" to round, "game" to game.toJson()))
return Json.Object("success" to true)
}
override fun delete(request: HttpServletRequest): Json {
val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
// only allow last round (if players have not been paired in the last round, it *may* be possible to be more laxist...)
if (round != tournament.lastRound()) badRequest("cannot delete games in other rounds but the last")
val payload = getArrayPayload(request)
val allPlayers = payload.size == 1 && payload[0] == "all"
if (allPlayers) {
tournament.games(round).clear()
} else {
payload.forEach {
val id = (it as Number).toInt()
tournament.games(round).remove(id)
}
}
tournament.dispatchEvent(gamesDeleted, Json.Object("round" to round, "games" to payload))
return Json.Object("success" to true)
}
}

View File

@@ -0,0 +1,50 @@
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.Player
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, response: HttpServletResponse): Json? {
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}")
}
}
override fun post(request: HttpServletRequest): Json {
val tournament = getTournament(request)
val payload = getObjectPayload(request)
val player = Player.fromJson(payload)
tournament.players[player.id] = player
tournament.dispatchEvent(playerAdded, player.toJson())
return Json.Object("success" to true, "id" to player.id)
}
override fun put(request: HttpServletRequest): Json {
val tournament = getTournament(request)
val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector")
val player = tournament.players[id] ?: badRequest("invalid player id")
val payload = getObjectPayload(request)
val updated = Player.fromJson(payload, player)
tournament.players[updated.id] = updated
tournament.dispatchEvent(playerUpdated, player.toJson())
return Json.Object("success" to true)
}
override fun delete(request: HttpServletRequest): Json {
val tournament = getTournament(request)
val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector")
tournament.players.remove(id) ?: badRequest("invalid player id")
tournament.dispatchEvent(playerDeleted, Json.Object("id" to id))
return Json.Object("success" to true)
}
}

View File

@@ -0,0 +1,30 @@
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.Game
import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.web.Event
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
object ResultsHandler: PairgothApiHandler {
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(round).values
return games.map { it.toJson() }.toJsonArray()
}
override fun put(request: HttpServletRequest): Json {
val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
val payload = getObjectPayload(request)
val game = tournament.games(round)[payload.getInt("id")] ?: badRequest("invalid game id")
game.result = Game.Result.fromSymbol(payload.getChar("result") ?: badRequest("missing result"))
tournament.dispatchEvent(Event.resultUpdated, Json.Object("round" to round, "data" to game))
return Json.Object("success" to true)
}
}

View File

@@ -0,0 +1,4 @@
package org.jeudego.pairgoth.api
object StandingsHandler: PairgothApiHandler {
}

View File

@@ -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
tournament.dispatchEvent(teamAdded, 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
tournament.dispatchEvent(teamUpdated, 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")
tournament.dispatchEvent(teamDeleted, Json.Object("id" to id))
return Json.Object("success" to true)
}
}

View File

@@ -0,0 +1,78 @@
package org.jeudego.pairgoth.api
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
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, response: HttpServletResponse): Json? {
val accept = request.getHeader("Accept")
return when (val id = getSelector(request)?.toIntOrNull()) {
null -> Json.Array(Store.getTournamentsIDs())
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")
}
}
}
override fun post(request: HttpServletRequest): Json {
val tournament = when (val payload = request.getAttribute(PAYLOAD_KEY)) {
is Json.Object -> Tournament.fromJson(getObjectPayload(request))
is Element -> OpenGotha.import(payload)
else -> badRequest("missing or invalid payload")
}
Store.addTournament(tournament)
tournament.dispatchEvent(tournamentAdded, tournament.toJson())
return Json.Object("success" to true, "id" to tournament.id)
}
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)
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)
// copy players, games, criteria (this copy should be provided by the Tournament class - CB TODO)
updated.players.putAll(tournament.players)
if (tournament is TeamTournament && updated is TeamTournament) {
updated.teams.putAll(tournament.teams)
}
for (round in 1..tournament.lastRound()) updated.games(round).apply {
clear()
putAll(tournament.games(round))
}
updated.criteria.addAll(tournament.criteria)
Store.replaceTournament(updated)
tournament.dispatchEvent(tournamentUpdated, tournament.toJson())
return Json.Object("success" to true)
}
override fun delete(request: HttpServletRequest): Json {
val tournament = getTournament(request)
Store.deleteTournament(tournament)
tournament.dispatchEvent(tournamentDeleted, Json.Object("id" to tournament.id))
return Json.Object("success" to true)
}
}

View File

@@ -0,0 +1,286 @@
package org.jeudego.pairgoth.ext
import org.jeudego.pairgoth.model.CanadianByoyomi
import org.jeudego.pairgoth.model.FischerTime
import org.jeudego.pairgoth.model.Game
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
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
import org.jeudego.pairgoth.util.booleanAttr
import org.jeudego.pairgoth.util.childrenArrayOf
import org.jeudego.pairgoth.util.dateAttr
import org.jeudego.pairgoth.util.doubleAttr
import org.jeudego.pairgoth.util.find
import org.jeudego.pairgoth.util.get
import org.jeudego.pairgoth.util.intAttr
import org.jeudego.pairgoth.util.objectOf
import org.jeudego.pairgoth.util.optBoolean
import org.jeudego.pairgoth.util.stringAttr
import org.w3c.dom.Element
import java.util.*
class OpenGothaFormat(xml: Element): XmlFormat(xml) {
val Players by childrenArrayOf<Player>()
val Games by childrenArrayOf<Game>()
val TournamentParameterSet by objectOf<Params>()
class Player(xml: Element): XmlFormat(xml) {
val agaId by stringAttr()
val club by stringAttr()
val country by stringAttr()
val egfPin by stringAttr()
val ffgLicence by stringAttr()
val firstName by stringAttr()
val name by stringAttr()
val participating by stringAttr()
val rank by stringAttr()
val rating by intAttr()
}
class Game(xml: Element): XmlFormat(xml) {
val blackPlayer by stringAttr()
val whitePlayer by stringAttr()
val handicap by intAttr()
val knownColor by booleanAttr()
val result by stringAttr()
val roundNumber by intAttr()
}
class Params(xml: Element): XmlFormat(xml) {
val GeneralParameterSet by objectOf<GenParams>()
val HandicapParameterSet by objectOf<HandicapParams>()
val PairingParameterSet by objectOf<PairingParams>()
class GenParams(xml: Element): XmlFormat(xml) {
val bInternet by optBoolean()
val basicTime by intAttr()
val beginDate by dateAttr()
val canByoYomiTime by intAttr()
val complementaryTimeSystem by stringAttr()
val endDate by dateAttr()
val fisherTime by intAttr()
val genCountNotPlayedGamesAsHalfPoint by booleanAttr()
val genMMBar by stringAttr()
val genMMFloor by stringAttr()
val komi by doubleAttr()
val location by stringAttr()
val name by stringAttr()
val nbMovesCanTime by intAttr()
val numberOfCategories by intAttr()
val numberOfRounds by intAttr()
val shortName by stringAttr()
val size by intAttr()
val stdByoYomiTime by intAttr()
}
class HandicapParams(xml: Element): XmlFormat(xml) {
val hdBasedOnMMS by booleanAttr()
val hdCeiling by intAttr()
val hdCorrection by intAttr()
val hdNoHdRankThreshold by stringAttr()
}
class PairingParams(xml: Element): XmlFormat(xml) {
val paiMaSeedSystem1 by stringAttr()
val paiMaSeedSystem2 by stringAttr()
}
}
}
object OpenGotha {
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 = StandardTournament(
id = Store.nextTournamentId,
type = Tournament.Type.INDIVIDUAL, // CB for now, TODO
name = genParams.name,
shortName = genParams.shortName,
startDate = genParams.beginDate,
endDate = genParams.endDate,
country = "FR", // no country in opengotha format
location = genParams.location,
online = genParams.bInternet ?: false,
timeSystem = when (genParams.complementaryTimeSystem) {
"SUDDENDEATH" -> SuddenDeath(genParams.basicTime)
"STDBYOYOMI" -> StandardByoyomi(genParams.basicTime, genParams.stdByoYomiTime, 1) // no periods?
"CANBYOYOMI" -> CanadianByoyomi(genParams.basicTime, genParams.canByoYomiTime, genParams.nbMovesCanTime)
"FISCHER" -> FischerTime(genParams.basicTime, genParams.fisherTime)
else -> throw Error("missing byoyomi type")
},
pairing = when (handParams.hdCeiling) {
0 -> Swiss(
when (pairingParams.paiMaSeedSystem1) {
"SPLITANDFOLD" -> Swiss.Method.SPLIT_AND_FOLD
"SPLITANDRANDOM" -> Swiss.Method.SPLIT_AND_RANDOM
"SPLITANDSLIP" -> Swiss.Method.SPLIT_AND_SLIP
else -> throw Error("unknown swiss pairing method")
},
when (pairingParams.paiMaSeedSystem2) {
"SPLITANDFOLD" -> Swiss.Method.SPLIT_AND_FOLD
"SPLITANDRANDOM" -> Swiss.Method.SPLIT_AND_RANDOM
"SPLITANDSLIP" -> Swiss.Method.SPLIT_AND_SLIP
else -> throw Error("unknown swiss pairing method")
}
)
else -> MacMahon() // TODO
},
rounds = genParams.numberOfRounds
)
val canonicMap = mutableMapOf<String, Int>()
imported.Players.map { player ->
Player(
id = Store.nextPlayerId,
name = player.name,
firstname = player.firstName,
rating = player.rating,
rank = Pairable.parseRank(player.rank),
country = player.country,
club = player.club
).also {
canonicMap.put("${player.name}${player.firstName}".uppercase(Locale.ENGLISH), it.id)
}
}.associateByTo(tournament.players) { it.id }
val gamesPerRound = imported.Games.groupBy {
it.roundNumber
}.values.map {
it.map { game ->
Game(
id = Store.nextGameId,
black = canonicMap[game.blackPlayer] ?: throw Error("player not found: ${game.blackPlayer}"),
white = canonicMap[game.whitePlayer] ?: throw Error("player not found: ${game.whitePlayer}"),
handicap = game.handicap,
result = when (game.result) {
"RESULT_UNKNOWN" -> Game.Result.UNKNOWN
"RESULT_WHITEWINS" -> Game.Result.WHITE
"RESULT_BLACKWINS" -> Game.Result.BLACK
"RESULT_EQUAL" -> Game.Result.JIGO
"RESULT_BOTHWIN" -> Game.Result.BOTHWIN
"RESULT_BOTHLOOSE" -> Game.Result.BOTHLOOSE
else -> throw Error("unhandled result: ${game.result}")
}
)
}.associateBy { it.id }.toMutableMap()
}
gamesPerRound.forEachIndexed { index, games ->
tournament.games(index).putAll(games)
}
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>
${(1..tournament.lastRound()).map { tournament.games(it) }.flatMapIndexed { index, games ->
games.values.mapIndexed { table, game ->
Triple(index + 1, 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"
}
}" roundNumber="${
round
}" 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

@@ -0,0 +1,11 @@
package org.jeudego.pairgoth.model
import com.republicate.kson.Json
typealias ID = Int
fun String.toID() = toInt()
fun String.toIDOrNull() = toIntOrNull()
fun Number.toID() = toInt()
fun Json.Object.getID(key: String) = getInt(key)
fun Json.Array.getID(index: Int) = getInt(index)

View File

@@ -0,0 +1,47 @@
package org.jeudego.pairgoth.model
import com.republicate.kson.Json
import org.jeudego.pairgoth.model.Game.Result.*
import java.util.*
data class Game(
val id: ID,
var white: ID,
var black: ID,
var handicap: Int = 0,
var result: Result = UNKNOWN
) {
companion object {}
enum class Result(val symbol: Char) {
UNKNOWN('?'),
BLACK('b'),
WHITE('w'),
JIGO('='),
CANCELLED('X'),
BOTHWIN('#'),
BOTHLOOSE('0');
companion object {
private val byChar = Result.values().associateBy { it.symbol }
fun fromSymbol(c: Char) = byChar[c] ?: throw Error("unknown result symbol: $c")
}
}
}
// serialization
fun Game.toJson() = Json.Object(
"id" to id,
"w" to white,
"b" to black,
"h" to handicap,
"r" to "${result.symbol}"
)
fun Game.Companion.fromJson(json: Json.Object) = Game(
id = json.getID("id") ?: throw Error("missing game id"),
white = json.getID("white") ?: throw Error("missing white player"),
black = json.getID("black") ?: throw Error("missing black player"),
handicap = json.getInt("handicap") ?: 0,
result = json.getChar("result")?.let { Game.Result.fromSymbol(it) } ?: UNKNOWN
)

View File

@@ -0,0 +1,89 @@
package org.jeudego.pairgoth.model
import com.republicate.kson.Json
import com.republicate.kson.toJsonArray
import org.jeudego.pairgoth.api.ApiHandler
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.store.Store
import kotlin.math.roundToInt
// Pairable
sealed class Pairable(val id: ID, val name: String, open val rating: Int, open val rank: Int) {
companion object {}
abstract fun toJson(): Json.Object
abstract val club: String?
abstract val country: String?
val skip = mutableSetOf<Int>() // skipped rounds
}
object ByePlayer: Pairable(0, "bye", 0, Int.MIN_VALUE) {
override fun toJson(): Json.Object {
throw Error("bye player should never be serialized")
}
override val club = "none"
override val country = "none"
}
fun Pairable.displayRank(): String = when {
rank < 0 -> "${-rank}k"
rank < 10 -> "${rank + 1}d"
else -> "${rank - 9}p"
}
private val rankRegex = Regex("(\\d+)([kdp])", RegexOption.IGNORE_CASE)
fun Pairable.Companion.parseRank(rankStr: String): Int {
val (level, letter) = rankRegex.matchEntire(rankStr)?.destructured ?: throw Error("invalid rank: $rankStr")
val num = level.toInt()
if (num < 0 || letter != "k" && letter != "K" && num > 9) throw Error("invalid rank: $rankStr")
return when (letter.lowercase()) {
"k" -> -num
"d" -> num - 1
"p" -> num + 9
else -> throw Error("impossible")
}
}
// Player
class Player(
id: ID,
name: String,
var firstname: String,
rating: Int,
rank: Int,
override var country: String,
override var club: String
): Pairable(id, name, rating, rank) {
companion object
// used to store external IDs ("FFG" => FFG ID, "EGF" => EGF PIN, "AGA" => AGA ID ...)
val externalIds = mutableMapOf<String, String>()
override fun toJson(): Json.Object = Json.MutableObject(
"id" to id,
"name" to name,
"firstname" to firstname,
"rating" to rating,
"rank" to rank,
"country" to country,
"club" to club
).also {
if (skip.isNotEmpty()) it["skip"] = Json.Array(skip)
}
}
fun Player.Companion.fromJson(json: Json.Object, default: Player? = null) = Player(
id = json.getInt("id") ?: default?.id ?: Store.nextPlayerId,
name = json.getString("name") ?: default?.name ?: badRequest("missing name"),
firstname = json.getString("firstname") ?: default?.firstname ?: badRequest("missing firstname"),
rating = json.getInt("rating") ?: default?.rating ?: badRequest("missing rating"),
rank = json.getInt("rank") ?: default?.rank ?: badRequest("missing rank"),
country = json.getString("country") ?: default?.country ?: badRequest("missing country"),
club = json.getString("club") ?: default?.club ?: badRequest("missing club")
).also { player ->
player.skip.clear()
json.getArray("skip")?.let {
if (it.isNotEmpty()) player.skip.addAll(it.map { id -> (id as Number).toInt() })
}
}

View File

@@ -0,0 +1,89 @@
package org.jeudego.pairgoth.model
import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.Pairing.PairingType.*
import org.jeudego.pairgoth.model.MacMahon
import org.jeudego.pairgoth.model.RoundRobin
import org.jeudego.pairgoth.model.Swiss
import org.jeudego.pairgoth.pairing.MacMahonSolver
import org.jeudego.pairgoth.pairing.SwissSolver
import java.util.Random
sealed class Pairing(val type: PairingType, val weights: Weights = Weights()) {
companion object {}
enum class PairingType { SWISS, MACMAHON, ROUNDROBIN }
data class Weights(
val played: Double = 1_000_000.0, // players already met
val group: Double = 100_000.0, // different group
val handicap: Double = 50_000.0, // for each handicap stone
val score: Double = 10_000.0, // per difference of score or MMS
val place: Double = 1_000.0, // per difference of expected position for Swiss
val color: Double = 500.0, // per color unbalancing
val club: Double = 100.0, // same club weight
val country: Double = 50.0 // same country
)
abstract fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game>
}
fun Tournament<*>.historyBefore(round: Int) =
if (lastRound() == 0) emptyList()
else (0 until round).flatMap { games(round).values }
class Swiss(
var method: Method,
var firstRoundMethod: Method = method,
): Pairing(SWISS, Weights(
handicap = 0.0, // no handicap games anyway
club = 0.0,
country = 0.0
)) {
enum class Method { SPLIT_AND_FOLD, SPLIT_AND_RANDOM, SPLIT_AND_SLIP }
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
val actualMethod = if (round == 1) firstRoundMethod else method
return SwissSolver(tournament.historyBefore(round), pairables, weights, actualMethod).pair()
}
}
class MacMahon(
var bar: Int = 0,
var minLevel: Int = -30,
var reducer: Int = 1
): Pairing(MACMAHON) {
val groups = mutableListOf<Int>()
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
return MacMahonSolver(tournament.historyBefore(round), pairables, weights, mmBase = minLevel, mmBar = bar, reducer = reducer).pair()
}
}
class RoundRobin: Pairing(ROUNDROBIN) {
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
TODO()
}
}
// Serialization
fun Pairing.Companion.fromJson(json: Json.Object) = when (json.getString("type")?.let { Pairing.PairingType.valueOf(it) } ?: badRequest("missing pairing type")) {
SWISS -> Swiss(
method = json.getString("method")?.let { Swiss.Method.valueOf(it) } ?: badRequest("missing pairing method"),
firstRoundMethod = json.getString("firstRoundMethod")?.let { Swiss.Method.valueOf(it) } ?: json.getString("method")!!.let { Swiss.Method.valueOf(it) }
)
MACMAHON -> MacMahon(
bar = json.getInt("bar") ?: 0,
minLevel = json.getInt("minLevel") ?: -30,
reducer = json.getInt("reducer") ?: 1
)
ROUNDROBIN -> RoundRobin()
}
fun Pairing.toJson() = when (this) {
is Swiss ->
if (method == firstRoundMethod) Json.Object("type" to type.name, "method" to method.name)
else Json.Object("type" to type.name, "method" to method.name, "firstRoundMethod" to firstRoundMethod.name)
is MacMahon -> Json.Object("type" to type.name, "bar" to bar, "minLevel" to minLevel, "reducer" to reducer)
is RoundRobin -> Json.Object("type" to type.name)
}

View File

@@ -0,0 +1,8 @@
package org.jeudego.pairgoth.model
enum class Rules {
FRENCH,
JAPANESE,
CHINESE
// ...
}

View File

@@ -0,0 +1,94 @@
package org.jeudego.pairgoth.model
import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.TimeSystem.TimeSystemType.*
data class TimeSystem(
val type: TimeSystemType,
val mainTime: Int,
val increment: Int,
val maxTime: Int = Int.MAX_VALUE,
val byoyomi: Int,
val periods: Int,
val stones: Int
) {
companion object {}
enum class TimeSystemType { CANADIAN, STANDARD, FISCHER, SUDDEN_DEATH }
}
fun CanadianByoyomi(mainTime: Int, byoyomi: Int, stones: Int) =
TimeSystem(
type = CANADIAN,
mainTime = mainTime,
increment = 0,
byoyomi = byoyomi,
periods = 1,
stones = stones
)
fun StandardByoyomi(mainTime: Int, byoyomi: Int, periods: Int) =
TimeSystem(
type = STANDARD,
mainTime = mainTime,
increment = 0,
byoyomi = byoyomi,
periods = periods,
stones = 1
)
fun FischerTime(mainTime: Int, increment: Int, maxTime: Int = Int.MAX_VALUE) =
TimeSystem(
type = FISCHER,
mainTime = mainTime,
increment = increment,
maxTime = maxTime,
byoyomi = 0,
periods = 0,
stones = 0
)
fun SuddenDeath(mainTime: Int) =
TimeSystem(
type = SUDDEN_DEATH,
mainTime = mainTime,
increment = 0,
byoyomi = 0,
periods = 0,
stones = 0
)
// Serialization
fun TimeSystem.Companion.fromJson(json: Json.Object) =
when (json.getString("type")?.uppercase() ?: badRequest("missing timeSystem type")) {
"CANADIAN" -> CanadianByoyomi(
mainTime = json.getInt("mainTime") ?: badRequest("missing timeSystem mainTime"),
byoyomi = json.getInt("byoyomi") ?: badRequest("missing timeSystem byoyomi"),
stones = json.getInt("stones") ?: badRequest("missing timeSystem stones")
)
"STANDARD" -> StandardByoyomi(
mainTime = json.getInt("mainTime") ?: badRequest("missing timeSystem mainTime"),
byoyomi = json.getInt("byoyomi") ?: badRequest("missing timeSystem byoyomi"),
periods = json.getInt("periods") ?: badRequest("missing timeSystem periods")
)
"FISCHER" -> FischerTime(
mainTime = json.getInt("mainTime") ?: badRequest("missing timeSystem mainTime"),
increment = json.getInt("increment") ?: badRequest("missing timeSystem increment"),
maxTime = json.getInt("maxTime") ?: Integer.MAX_VALUE
)
"SUDDEN_DEATH" -> SuddenDeath(
mainTime = json.getInt("mainTime") ?: badRequest("missing timeSystem mainTime"),
)
else -> badRequest("invalid or missing timeSystem type")
}
fun TimeSystem.toJson() = when (type) {
TimeSystem.TimeSystemType.CANADIAN -> Json.Object("type" to type.name, "mainTime" to mainTime, "byoyomi" to byoyomi, "stones" to stones)
TimeSystem.TimeSystemType.STANDARD -> Json.Object("type" to type.name, "mainTime" to mainTime, "byoyomi" to byoyomi, "periods" to periods)
TimeSystem.TimeSystemType.FISCHER ->
if (maxTime == Int.MAX_VALUE) Json.Object("type" to type.name, "mainTime" to mainTime, "increment" to increment)
else Json.Object("type" to type.name, "mainTime" to mainTime, "increment" to increment, "maxTime" to maxTime)
TimeSystem.TimeSystemType.SUDDEN_DEATH -> Json.Object("type" to type.name, "mainTime" to mainTime)
}

View File

@@ -0,0 +1,216 @@
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 <P: Pairable>(
val id: ID,
val type: Type,
val name: String,
val shortName: String,
val startDate: LocalDate,
val endDate: LocalDate,
val country: String,
val location: String,
val online: Boolean,
val timeSystem: TimeSystem,
val rounds: Int,
val pairing: Pairing,
val rules: Rules = Rules.FRENCH,
val gobanSize: Int = 19,
val komi: Double = 7.5
) {
companion object {}
enum class Type(val playersNumber: Int, val individual: Boolean = true) {
INDIVIDUAL(1),
PAIRGO(2, false),
RENGO2(2, false),
RENGO3(3, false),
TEAM2(2),
TEAM3(3),
TEAM4(4),
TEAM5(5);
}
enum class Criterion {
NBW, MMS, SOS, SOSOS, SODOS
}
// players per id
abstract val players: MutableMap<ID, Player>
// pairables per id
protected val _pairables = mutableMapOf<ID, P>()
val pairables: Map<ID, Pairable> get() = _pairables
// pairing
fun pair(round: Int, pairables: List<Pairable>): List<Game> {
// Minimal check on round number.
// CB TODO - the complete check should verify, for each player, that he was either non pairable or implied in the previous round
if (round > games.size + 1) badRequest("previous round not paired")
if (round > rounds) badRequest("too many rounds")
val evenPairables =
if (pairables.size % 2 == 0) pairables
else pairables.toMutableList().also { it.add(ByePlayer) }
return pairing.pair(this, round, evenPairables).also { newGames ->
if (games.size < round) games.add(mutableMapOf())
games[round - 1].putAll( newGames.associateBy { it.id } )
}
}
// games per id for each round
private val games = mutableListOf<MutableMap<ID, Game>>()
fun games(round: Int) = games.getOrNull(round - 1) ?:
if (round > games.size + 1) throw Error("invalid round")
else mutableMapOf<ID, Game>().also { games.add(it) }
fun lastRound() = games.size
// standings criteria
val criteria = mutableListOf<Criterion>(
if (pairing.type == Pairing.PairingType.MACMAHON) Criterion.MMS else Criterion.NBW,
Criterion.SOS,
Criterion.SOSOS
)
}
// standard tournament of individuals
class StandardTournament(
id: ID,
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: ID,
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<ID, Player>()
val teams: MutableMap<ID, Team> = _pairables
inner class Team(id: ID, name: String): Pairable(id, name, 0, 0) {
val playerIds = mutableSetOf<ID>()
val teamPlayers: Set<Player> get() = playerIds.mapNotNull { players[id] }.toSet()
override val rating: Int get() = if (teamPlayers.isEmpty()) super.rating else (teamPlayers.sumOf { player -> player.rating.toDouble() } / players.size).roundToInt()
override val rank: Int get() = if (teamPlayers.isEmpty()) super.rank else (teamPlayers.sumOf { player -> player.rank.toDouble() } / players.size).roundToInt()
override val club: String? get() = teamPlayers.map { club }.distinct().let { if (it.size == 1) it[0] else null }
override val country: String? get() = teamPlayers.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()
)
val teamOfIndividuals: Boolean get() = type.individual
}
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
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
val tournament = 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")
)
json["pairables"]?.let { pairables ->
}
return tournament
}
fun Tournament<*>.toJson() = Json.Object(
"id" to id,
"type" to type.name,
"name" to name,
"shortName" to shortName,
"startDate" to startDate.toString(),
"endDate" to endDate.toString(),
"country" to country,
"location" to location,
"online" to online,
"komi" to komi,
"rules" to rules.name,
"gobanSize" to gobanSize,
"timeSystem" to timeSystem.toJson(),
"rounds" to rounds,
"pairing" to pairing.toJson()
)

View File

@@ -0,0 +1,28 @@
package org.jeudego.pairgoth.oauth
class FacebookHelper : OAuthHelper() {
override val name: String
get() = "facebook"
override fun getLoginURL(sessionId: String?): String {
return "https://www.facebook.com/v14.0/dialog/oauth?" +
"client_id=" + clientId +
"&redirect_uri=" + redirectURI +
"&scope=email" +
"&state=" + getState(sessionId!!)
}
override fun getAccessTokenURL(code: String): String? {
return "https://graph.facebook.com/v14.0/oauth/access_token?" +
"client_id=" + clientId +
"&redirect_uri=" + redirectURI +
"&client_secret=" + secret +
"&code=" + code
}
override fun getUserInfosURL(accessToken: String): String? {
return "https://graph.facebook.com/me?" +
"field=email" +
"&access_token=" + accessToken
}
}

View File

@@ -0,0 +1,18 @@
package org.jeudego.pairgoth.oauth
class GoogleHelper : OAuthHelper() {
override val name: String
get() = "google"
override fun getLoginURL(sessionId: String?): String {
return ""
}
override fun getAccessTokenURL(code: String): String? {
return null
}
override fun getUserInfosURL(accessToken: String): String? {
return null
}
}

View File

@@ -0,0 +1,18 @@
package org.jeudego.pairgoth.oauth
class InstagramHelper : OAuthHelper() {
override val name: String
get() = "instagram"
override fun getLoginURL(sessionId: String?): String {
return ""
}
override fun getAccessTokenURL(code: String): String? {
return null
}
override fun getUserInfosURL(accessToken: String): String? {
return null
}
}

View File

@@ -0,0 +1,77 @@
package org.jeudego.pairgoth.oauth
// In progress
import com.republicate.kson.Json
import org.jeudego.pairgoth.web.WebappManager
//import com.republicate.modality.util.AESCryptograph
//import com.republicate.modality.util.Cryptograph
import org.apache.commons.codec.binary.Base64
import org.slf4j.LoggerFactory
import java.io.IOException
import java.io.UnsupportedEncodingException
import java.net.URLEncoder
abstract class OAuthHelper {
abstract val name: String
abstract fun getLoginURL(sessionId: String?): String
protected val clientId: String
protected get() = WebappManager.getMandatoryProperty("oauth." + name + ".client_id")
protected val secret: String
protected get() = WebappManager.getMandatoryProperty("oauth." + name + ".secret")
protected val redirectURI: String?
protected get() = try {
val uri: String = WebappManager.Companion.getProperty("webapp.url") + "/oauth.html"
URLEncoder.encode(uri, "UTF-8")
} catch (uee: UnsupportedEncodingException) {
logger.error("could not encode redirect URI", uee)
null
}
protected fun getState(sessionId: String): String {
return name + ":" + encrypt(sessionId)
}
fun checkState(state: String, expectedSessionId: String): Boolean {
val foundSessionId = decrypt(state)
return expectedSessionId == foundSessionId
}
protected abstract fun getAccessTokenURL(code: String): String?
@Throws(IOException::class)
fun getAccessToken(code: String): String {
val json: Json.Object = Json.Object() // TODO - apiClient.get(getAccessTokenURL(code))
return json.getString("access_token")!! // ?!
}
protected abstract fun getUserInfosURL(accessToken: String): String?
@Throws(IOException::class)
fun getUserEmail(accessToken: String): String {
val json: Json.Object = Json.Object()
// TODO
// apiClient.get(getUserInfosURL(accessToken))
return json.getString("email") ?: throw IOException("could not fetch email")
}
companion object {
protected var logger = LoggerFactory.getLogger("oauth")
private const val salt = "0efd28fb53cbac42"
// private val sessionIdCrypto: Cryptograph = AESCryptograph().apply {
// init(salt)
// }
private fun encrypt(input: String): String {
return "TODO"
// return Base64.encodeBase64URLSafeString(sessionIdCrypto.encrypt(input))
}
private fun decrypt(input: String): String {
return "TODO"
// return sessionIdCrypto.decrypt(Base64.decodeBase64(input))
}
// TODO
// private val apiClient: ApiClient = ApiClient()
}
}

View File

@@ -0,0 +1,17 @@
package org.jeudego.pairgoth.oauth
object OauthHelperFactory {
private val facebook: OAuthHelper = FacebookHelper()
private val google: OAuthHelper = GoogleHelper()
private val instagram: OAuthHelper = InstagramHelper()
private val twitter: OAuthHelper = TwitterHelper()
fun getHelper(provider: String?): OAuthHelper {
return when (provider) {
"facebook" -> facebook
"google" -> google
"instagram" -> instagram
"twitter" -> twitter
else -> throw RuntimeException("wrong provider")
}
}
}

View File

@@ -0,0 +1,18 @@
package org.jeudego.pairgoth.oauth
class TwitterHelper : OAuthHelper() {
override val name: String
get() = "twitter"
override fun getLoginURL(sessionId: String?): String {
return ""
}
override fun getAccessTokenURL(code: String): String? {
return null
}
override fun getUserInfosURL(accessToken: String): String? {
return null
}
}

View File

@@ -0,0 +1,95 @@
package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.TeamTournament
open class HistoryHelper(protected val history: List<Game>) {
open fun playedTogether(p1: Pairable, p2: Pairable) = paired.contains(Pair(p1.id, p2.id))
open fun colorBalance(p: Pairable) = colorBalance[p.id]
open fun score(p: Pairable) = score[p.id]
open fun sos(p: Pairable) = sos[p.id]
open fun sosos(p: Pairable) = sosos[p.id]
open fun sodos(p: Pairable) = sodos[p.id]
protected val paired: Set<Pair<Int, Int>> by lazy {
(history.map { game ->
Pair(game.black, game.white)
} + history.map { game ->
Pair(game.white, game.black)
}).toSet()
}
private val colorBalance: Map<Int, Int> by lazy {
history.flatMap { game ->
listOf(Pair(game.white, +1), Pair(game.black, -1))
}.groupingBy { it.first }.fold(0) { acc, next ->
acc + next.second
}
}
private val score: Map<Int, Double> by lazy {
mutableMapOf<Int, Double>().apply {
history.forEach { game ->
when (game.result) {
Game.Result.BLACK -> put(game.black, getOrDefault(game.black, 0.0) + 1.0)
Game.Result.WHITE -> put(game.white, getOrDefault(game.white, 0.0) + 1.0)
Game.Result.BOTHWIN -> {
put(game.black, getOrDefault(game.black, 0.0) + 0.5)
put(game.white, getOrDefault(game.white, 0.0) + 0.5)
}
else -> {}
}
}
}
}
private val sos by lazy {
(history.map { game ->
Pair(game.black, score[game.white] ?: 0.0)
} + history.map { game ->
Pair(game.white, score[game.black] ?: 0.0)
}).groupingBy { it.first }.fold(0.0) { acc, next ->
acc + next.second
}
}
private val sosos by lazy {
(history.map { game ->
Pair(game.black, sos[game.white] ?: 0.0)
} + history.map { game ->
Pair(game.white, sos[game.black] ?: 0.0)
}).groupingBy { it.first }.fold(0.0) { acc, next ->
acc + next.second
}
}
private val sodos by lazy {
(history.map { game ->
Pair(game.black, if (game.result == Game.Result.BLACK) score[game.white] ?: 0.0 else 0.0)
} + history.map { game ->
Pair(game.white, if (game.result == Game.Result.WHITE) score[game.black] ?: 0.0 else 0.0)
}).groupingBy { it.first }.fold(0.0) { acc, next ->
acc + next.second
}
}
}
// CB TODO - a big problem with the current naive implementation is that the team score is -for now- the sum of team members individual scores
class TeamOfIndividualsHistoryHelper(history: List<Game>): HistoryHelper(history) {
private fun Pairable.asTeam() = this as TeamTournament.Team
override fun playedTogether(p1: Pairable, p2: Pairable) = paired.intersect(p1.asTeam().playerIds.first().let { id ->
(p2.asTeam()).playerIds.map {Pair(it, id) }
}.toSet()).isNotEmpty()
override fun score(p: Pairable) = p.asTeam().teamPlayers.map { super.score(it) ?: throw Error("unknown player id: #${it.id}") }.sum()
override fun sos(p:Pairable) = p.asTeam().teamPlayers.map { super.sos(it) ?: throw Error("unknown player id: #${it.id}") }.sum()
override fun sosos(p:Pairable) = p.asTeam().teamPlayers.map { super.sosos(it) ?: throw Error("unknown player id: #${it.id}") }.sum()
override fun sodos(p:Pairable) = p.asTeam().teamPlayers.map { super.sodos(it) ?: throw Error("unknown player id: #${it.id}") }.sum()
}

View File

@@ -0,0 +1,45 @@
package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.Pairing
import org.jeudego.pairgoth.model.Swiss.Method.*
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
import kotlin.math.sign
class MacMahonSolver(history: List<Game>, pairables: List<Pairable>, weights: Pairing.Weights, val mmBase: Int, val mmBar: Int, val reducer: Int): Solver(history, pairables, weights) {
val Pairable.mms get() = mmBase + score
// CB TODO - configurable criteria
override fun sort(p: Pairable, q: Pairable): Int =
if (p.mms != q.mms) ((q.mms - p.mms) * 1000).toInt()
else if (p.sos != q.sos) ((q.sos - p.sos) * 1000).toInt()
else if (p.sosos != q.sosos) ((q.sosos - p.sosos) * 1000).toInt()
else 0
override fun weight(black: Pairable, white: Pairable): Double {
var weight = 0.0
if (black.played(white)) weight += weights.played
if (black.club == white.club) weight += weights.club
if (black.country == white.country) weight += weights.country
weight += (abs(black.colorBalance + 1) + abs(white.colorBalance - 1)) * weights.color
// MacMahon specific
weight += Math.abs(black.mms - white.mms) * weights.score
if (sign(mmBar - black.mms) != sign(mmBar - white.mms)) weight += weights.group
if (black.mms < mmBar && white.mms < mmBar && abs(black.mms - white.mms) > reducer) {
if (black.mms > white.mms) weight = Double.NaN
else weight = handicap(black, white) * weights.handicap
}
return weight
}
override fun handicap(black: Pairable, white: Pairable) =
if (black.mms > mmBar || white.mms > mmBar || abs(black.mms - white.mms) < reducer || black.mms > white.mms) 0
else (white.mms - black.mms - reducer).roundToInt()
}

View File

@@ -0,0 +1,111 @@
package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Game.Result.*
import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.Pairing
import org.jeudego.pairgoth.model.TeamTournament
import org.jeudego.pairgoth.store.Store
import org.jgrapht.alg.matching.blossom.v5.KolmogorovWeightedPerfectMatching
import org.jgrapht.alg.matching.blossom.v5.ObjectiveSense
import org.jgrapht.graph.DefaultWeightedEdge
import org.jgrapht.graph.SimpleDirectedWeightedGraph
import org.jgrapht.graph.SimpleWeightedGraph
import org.jgrapht.graph.builder.GraphBuilder
import java.util.*
interface HistoryDigester {
val colorBalance: Map<Int, Int>
val score: Map<Int, Double>
val sos: Map<Int, Double>
val sosos: Map<Int, Double>
val sodos: Map<Int, Double>
}
sealed class Solver(history: List<Game>, val pairables: List<Pairable>, val weights: Pairing.Weights) {
companion object {
val rand = Random(/* seed from properties - TODO */)
}
open fun sort(p: Pairable, q: Pairable): Int = 0 // no sort by default
abstract fun weight(black: Pairable, white: Pairable): Double
open fun handicap(black: Pairable, white: Pairable) = 0
open fun games(black: Pairable, white: Pairable): List<Game> {
// CB TODO team of individuals pairing
return listOf(Game(id = Store.nextGameId, black = black.id, white = white.id, handicap = handicap(black, white)))
}
fun pair(): List<Game> {
// check that at this stage, we have an even number of pairables
if (pairables.size % 2 != 0) throw Error("expecting an even number of pairables")
val builder = GraphBuilder(SimpleDirectedWeightedGraph<Pairable, DefaultWeightedEdge>(DefaultWeightedEdge::class.java))
for (i in sortedPairables.indices) {
for (j in i + 1 until n) {
val p = pairables[i]
val q = pairables[j]
weight(p, q).let { if (it != Double.NaN) builder.addEdge(p, q, it) }
weight(q, p).let { if (it != Double.NaN) builder.addEdge(q, p, it) }
}
}
val graph = builder.build()
val matching = KolmogorovWeightedPerfectMatching(graph, ObjectiveSense.MINIMIZE)
val solution = matching.matching
val result = solution.flatMap {
games(black = graph.getEdgeSource(it) , white = graph.getEdgeTarget(it))
}
return result
}
// Calculation parameters
val n = pairables.size
private val historyHelper =
if (pairables.first().let { it is TeamTournament.Team && it.teamOfIndividuals }) TeamOfIndividualsHistoryHelper(history)
else HistoryHelper(history)
// pairables sorted using overloadable sort function
private val sortedPairables by lazy {
pairables.sortedWith(::sort)
}
// place (among sorted pairables)
val Pairable.place: Int get() = _place[id]!!
private val _place by lazy {
sortedPairables.mapIndexed { index, pairable ->
Pair(pairable.id, index)
}.toMap()
}
// placeInGroup (of same score) : Pair(place, groupSize)
val Pairable.placeInGroup: Pair<Int, Int> get() = _placeInGroup[id]!!
private val _placeInGroup by lazy {
sortedPairables.groupBy {
it.score
}.values.flatMap { group ->
group.mapIndexed { index, pairable ->
Pair(pairable.id, Pair(index, group.size))
}
}.toMap()
}
// already paired players map
fun Pairable.played(other: Pairable) = historyHelper.playedTogether(this, other)
// color balance (nw - nb)
val Pairable.colorBalance: Int get() = historyHelper.colorBalance(this) ?: 0
// score (number of wins)
val Pairable.score: Double get() = historyHelper.score(this) ?: 0.0
// sos
val Pairable.sos: Double get() = historyHelper.sos(this) ?: 0.0
// sosos
val Pairable.sosos: Double get() = historyHelper.sosos(this) ?: 0.0
// sodos
val Pairable.sodos: Double get() = historyHelper.sodos(this) ?: 0.0
}

View File

@@ -0,0 +1,39 @@
package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.Pairing
import org.jeudego.pairgoth.model.Swiss
import org.jeudego.pairgoth.model.Swiss.Method.*
import kotlin.math.abs
class SwissSolver(history: List<Game>, pairables: List<Pairable>, weights: Pairing.Weights, val method: Swiss.Method): Solver(history, pairables, weights) {
override fun sort(p: Pairable, q: Pairable): Int =
when (p.score) {
q.score -> q.rating - p.rating
else -> ((q.score - p.score) * 1000).toInt()
}
override fun weight(black: Pairable, white: Pairable): Double {
var weight = 0.0
if (black.played(white)) weight += weights.played
if (black.score != white.score) {
val placeWeight =
if (black.score > white.score) (black.placeInGroup.second + white.placeInGroup.first) * weights.place
else (white.placeInGroup.second + black.placeInGroup.first) * weights.place
weight += abs(black.score - white.score) * weights.score + placeWeight
} else {
weight += when (method) {
SPLIT_AND_FOLD ->
if (black.placeInGroup.first > white.placeInGroup.first) abs(black.placeInGroup.first - (white.placeInGroup.second - white.placeInGroup.first)) * weights.place
else abs(white.placeInGroup.first - (black.placeInGroup.second - black.placeInGroup.first)) * weights.place
SPLIT_AND_RANDOM -> rand.nextDouble() * black.placeInGroup.second * weights.place
SPLIT_AND_SLIP -> abs(abs(black.placeInGroup.first - white.placeInGroup.first) - black.placeInGroup.second) * weights.place
}
}
weight += (abs(black.colorBalance + 1) + abs(white.colorBalance - 1)) * weights.color
return weight
}
}

View File

@@ -0,0 +1,115 @@
package org.jeudego.pairgoth.store
import com.republicate.kson.Json
import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.ID
import org.jeudego.pairgoth.model.Player
import org.jeudego.pairgoth.model.TeamTournament
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.fromJson
import org.jeudego.pairgoth.model.getID
import org.jeudego.pairgoth.model.toID
import org.jeudego.pairgoth.model.toJson
import java.nio.file.Path
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import kotlin.io.path.readText
import kotlin.io.path.useDirectoryEntries
private const val LEFT_PAD = 6 // left padding of IDs with '0' in filename
private fun Tournament<*>.filename() = "${id.toString().padStart(LEFT_PAD, '0')}-${shortName}.tour"
class FileStore(pathStr: String): StoreImplementation {
companion object {
private val filenameRegex = Regex("^(\\d+)-.*\\.tour$")
private val timestampFormat: DateFormat = SimpleDateFormat("yyyyMMddHHmmss")
private val timestamp: String get() = timestampFormat.format(Date())
}
private val path = Path.of(pathStr).also {
val file = it.toFile()
if (!file.mkdirs() && !file.isDirectory) throw Error("Property pairgoth.store.file.path must be a directory")
}
init {
_nextTournamentId.set(getTournamentsIDs().maxOrNull() ?: 0.toID())
}
override fun getTournamentsIDs(): Set<ID> {
return path.useDirectoryEntries("*.tour") { entries ->
entries.mapNotNull { entry ->
filenameRegex.matchEntire(entry.fileName.toString())?.groupValues?.get(1)?.toID()
}.toSet()
}
}
override fun addTournament(tournament: Tournament<*>) {
val filename = tournament.filename()
val file = path.resolve(filename).toFile()
if (file.exists()) throw Error("File $filename already exists")
val json = Json.MutableObject(tournament.toJson())
json["players"] = Json.Array(tournament.players.values.map { it.toJson() })
if (tournament is TeamTournament) {
json["teams"] = Json.Array(tournament.teams.values.map { it.toJson() })
}
json["games"] = Json.Array((1..tournament.lastRound()).map { round -> tournament.games(round).values.map { it.toJson() } });
file.printWriter().use { out ->
out.println(json.toPrettyString())
}
}
override fun getTournament(id: ID): Tournament<*>? {
val file = path.useDirectoryEntries("${id.toString().padStart(LEFT_PAD, '0')}-*.tour") { entries ->
entries.map { entry ->
entry.fileName.toString()
}.firstOrNull() ?: throw Error("no such tournament")
}
val json = Json.parse(path.resolve(file).readText())?.asObject() ?: throw Error("could not read tournament")
val tournament = Tournament.fromJson(json)
val players = json["players"] as Json.Array? ?: Json.Array()
tournament.players.putAll(
players.associate {
(it as Json.Object).let { player ->
Pair(player.getID("id") ?: throw Error("invalid tournament file"), Player.fromJson(player))
}
}
)
if (tournament is TeamTournament) {
val teams = json["teams"] as Json.Array? ?: Json.Array()
tournament.teams.putAll(
teams.associate {
(it as Json.Object).let { team ->
Pair(team.getID("id") ?: throw Error("invalid tournament file"), tournament.teamFromJson(team))
}
}
)
}
val games = json["games"] as Json.Array? ?: Json.Array()
(1..games.size).forEach { round ->
tournament.games(round).putAll(
games.associate {
(it as Json.Object).let { game ->
Pair(game.getID("id") ?: throw Error("invalid tournament file"), Game.fromJson(game))
}
}
)
}
return tournament
}
override fun replaceTournament(tournament: Tournament<*>) {
val filename = tournament.filename()
val file = path.resolve(filename).toFile()
if (!file.exists()) throw Error("File $filename does not exist")
file.renameTo(path.resolve(filename + "-${timestamp}").toFile())
addTournament(tournament)
}
override fun deleteTournament(tournament: Tournament<*>) {
val filename = tournament.filename()
val file = path.resolve(filename).toFile()
if (!file.exists()) throw Error("File $filename does not exist")
file.renameTo(path.resolve(filename + "-${timestamp}").toFile())
}
}

View File

@@ -0,0 +1,28 @@
package org.jeudego.pairgoth.store
import org.jeudego.pairgoth.model.ID
import org.jeudego.pairgoth.model.Tournament
class MemoryStore: StoreImplementation {
private val tournaments = mutableMapOf<ID, Tournament<*>>()
override fun getTournamentsIDs(): Set<ID> = tournaments.keys
override fun addTournament(tournament: Tournament<*>) {
if (tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} already exists")
tournaments[tournament.id] = tournament
}
override fun getTournament(id: ID) = tournaments[id]
override fun replaceTournament(tournament: Tournament<*>) {
if (!tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} not known")
tournaments[tournament.id] = tournament
}
override fun deleteTournament(tournament: Tournament<*>) {
if (!tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} not known")
tournaments.remove(tournament.id)
}
}

View File

@@ -0,0 +1,19 @@
package org.jeudego.pairgoth.store
import org.jeudego.pairgoth.model.ID
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.web.WebappManager
import java.util.concurrent.atomic.AtomicInteger
private fun createStoreImplementation(): StoreImplementation {
return when (val storeProperty = WebappManager.getProperty("store") ?: "memory") {
"memory" -> MemoryStore()
"file" -> {
val filePath = WebappManager.getMandatoryProperty("store.file.path") ?: "."
FileStore(filePath)
}
else -> throw Error("unknown store: $storeProperty")
}
}
object Store: StoreImplementation by createStoreImplementation()

View File

@@ -0,0 +1,22 @@
package org.jeudego.pairgoth.store
import org.jeudego.pairgoth.model.ID
import org.jeudego.pairgoth.model.Tournament
import java.util.concurrent.atomic.AtomicInteger
internal val _nextTournamentId = AtomicInteger()
internal val _nextPlayerId = AtomicInteger()
internal val _nextGameId = AtomicInteger()
interface StoreImplementation {
val nextTournamentId get() = _nextTournamentId.incrementAndGet()
val nextPlayerId get() = _nextPlayerId.incrementAndGet()
val nextGameId get() = _nextGameId.incrementAndGet()
fun getTournamentsIDs(): Set<ID>
fun addTournament(tournament: Tournament<*>)
fun getTournament(id: ID): Tournament<*>?
fun replaceTournament(tournament: Tournament<*>)
fun deleteTournament(tournament: Tournament<*>)
}

View File

@@ -0,0 +1,18 @@
package org.jeudego.pairgoth.util
import com.diogonunes.jcolor.Ansi
import com.diogonunes.jcolor.AnsiFormat
import com.diogonunes.jcolor.Attribute
private val blue = AnsiFormat(Attribute.BRIGHT_BLUE_TEXT())
private val green = AnsiFormat(Attribute.BRIGHT_GREEN_TEXT())
private val red = AnsiFormat(Attribute.BRIGHT_RED_TEXT())
private val bold = AnsiFormat(Attribute.BOLD())
object Colorizer {
fun blue(str: String) = Ansi.colorize(str, blue)
fun green(str: String) = Ansi.colorize(str, green)
fun red(str: String) = Ansi.colorize(str, red)
fun bold(str: String) = Ansi.colorize(str, bold)
}

View File

@@ -0,0 +1,25 @@
package org.jeudego.pairgoth.util
import com.republicate.kson.Json
import java.io.Reader
import java.io.Writer
fun Json.Companion.parse(reader: Reader) = Json.Companion.parse(object: Json.Input {
override fun read() = reader.read().toChar()
})
fun Json.toString(writer: Writer) = toString(object: Json.Output {
override fun writeChar(c: Char): Json.Output {
writer.write(c.code)
return this
}
override fun writeString(s: String): Json.Output {
writer.write(s)
return this
}
override fun writeString(s: String, from: Int, to: Int): Json.Output {
writer.write(s, from, to)
return this
}
})

View File

@@ -0,0 +1,362 @@
package org.jeudego.pairgoth.util
import com.republicate.kson.Json
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import org.w3c.dom.Element
import org.w3c.dom.ElementTraversal
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeFormatterBuilder
import java.time.temporal.ChronoField
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
// "XMLFormat" xml parsing and formatting utility class
// It is currently being packaged as an external open source library.
// See opengotha import code as a self-documenting example for how to use this class
open class XmlFormat(val xml: Element)
// standard types delegates
fun XmlFormat.string() = StringXmlDelegate(xml.element())
fun XmlFormat.optString() = OptionalStringXmlDelegate(xml.element())
fun XmlFormat.boolean() = BooleanXmlDelegate(xml.element())
fun XmlFormat.optBoolean() = OptionalBooleanXmlDelegate(xml.element())
fun XmlFormat.int() = IntXmlDelegate(xml.element())
fun XmlFormat.optInt() = OptionalIntXmlDelegate(xml.element())
fun XmlFormat.long() = LongXmlDelegate(xml.element())
fun XmlFormat.optLong() = OptionalLongXmlDelegate(xml.element())
fun XmlFormat.double() = DoubleXmlDelegate(xml.element())
//fun XmlFormat.optDouble() = OptinalDoubleXmlDelegate(xml.element())
inline fun <reified E: Enum<*>> XmlFormat.enum() = EnumXmlDelegate<E>(xml.element(), E::class)
fun XmlFormat.date(format: String = ISO_LOCAL_DATE_FORMAT) = DateXmlDelegate(xml.element(), format)
fun XmlFormat.datetime(format: String = ISO_LOCAL_DATETIME_FORMAT) = DateTimeXmlDelegate(xml.element(), format)
inline fun <reified T: XmlFormat> XmlFormat.childrenArrayOf() = ArrayXmlDelegate(xml, T::class)
inline fun <reified T: XmlFormat> XmlFormat.childrenArrayOf(tagName: String) = ChildrenArrayXmlDelegate(xml, tagName, T::class)
inline fun <reified T: XmlFormat> XmlFormat.mutableArrayOf() = MutableArrayXmlDelegate(xml, T::class)
inline fun <reified T: XmlFormat> XmlFormat.objectOf() = ObjectXmlDelegate(xml, T::class)
// standard type delegates for attributes
fun XmlFormat.stringAttr() = StringXmlAttrDelegate(xml.element())
fun XmlFormat.booleanAttr() = BooleanXmlAttrDelegate(xml.element())
fun XmlFormat.intAttr() = IntXmlAttrDelegate(xml.element())
fun XmlFormat.longAttr() = LongXmlAttrDelegate(xml.element())
fun XmlFormat.doubleAttr() = DoubleXmlAttrDelegate(xml.element())
fun XmlFormat.dateAttr(format: String = ISO_LOCAL_DATE_FORMAT) = DateXmlAttrDelegate(xml.element(), format)
// xpath delegates
fun XmlFormat.string(xpath: String) = StringXmlAttrDelegate(xml.element().find(xpath)[0].element())
fun XmlFormat.boolean(xpath: String) = StringXmlAttrDelegate(xml.element().find(xpath)[0].element())
fun XmlFormat.int(xpath: String) = StringXmlAttrDelegate(xml.element().find(xpath)[0].element())
fun XmlFormat.long(xpath: String) = StringXmlAttrDelegate(xml.element().find(xpath)[0].element())
// Helper classes and functions
private fun error(propName: String): Nothing { throw Error("missing property $propName") }
open class OptionalStringXmlDelegate(val xml: Element) {
open operator fun getValue(thisRef: Any?, property: KProperty<*>): String? = xml.childOrNull(property.name)?.value()
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) { value?.let { xml.child(property.name).textContent = value } }
}
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 class OptionalBooleanXmlDelegate(val xml: Element) {
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Boolean? = Json.TypeUtils.toBoolean(xml.childOrNull(property.name)?.value())
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean?) { value?.let { xml.child(property.name).textContent = value.toString() } }
}
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 class OptionalIntXmlDelegate(val xml: Element) {
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Int? = Json.TypeUtils.toInt(xml.childOrNull(property.name)?.value())
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int?) { value?.let { xml.child(property.name).textContent = value.toString() } }
}
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() } }
}
open class OptionalLongXmlDelegate(val xml: Element) {
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Long? = Json.TypeUtils.toLong(xml.childOrNull(property.name)?.value())
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Long?) { value?.let { xml.child(property.name).textContent = value.toString() } }
}
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() } }
}
open class OptionalDoubleXmlDelegate(val xml: Element) {
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Double? = Json.TypeUtils.toDouble(xml.childOrNull(property.name)?.value())
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Double?) { value?.let { xml.child(property.name).textContent = value.toString() } }
}
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() } }
}
open class OptionalEnumXmlDelegate<E: Enum<*>> (val xml: Element, private val kclass: KClass<E>) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): E? {
val enumValues = kclass.java.enumConstants as Array<E>
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() } }
}
open class EnumXmlDelegate<E: Enum<*>> (val xml: Element, private val kclass: KClass<E>) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): E {
val enumValues = kclass.java.enumConstants as Array<E>
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() } }
}
const val ISO_LOCAL_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"
const val ISO_LOCAL_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"
const val ISO_UTC_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"
const val ISO_ZONED_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssXXX"
const val ISO_LOCAL_YMD = "yyyyMMdd"
const val ISO_LOCAL_YMDHM = "yyyyMMddHHmm"
const val LOCAL_FRENCHY = "dd/MM/yyyy HH:mm"
internal fun dateTimeFormat(format: String): DateTimeFormatter {
val builder = DateTimeFormatterBuilder()
if (format.startsWith("yyyy")) {
// workaround Java bug
builder.appendValue(ChronoField.YEAR_OF_ERA, 4)
.appendPattern(format.substring(4))
} else {
builder.appendPattern(format)
}
builder.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
return builder.toFormatter()
}
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() } } }
}
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() } } }
}
open class OptionalDateTimeXmlDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATETIME_FORMAT) {
private val format = dateTimeFormat(formatString)
//FB TODO ** To rewrite
//open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDateTime? = xml.childOrNull(property.name)?.value()?.let { LocalDateTime.parse(it, format) }
open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDateTime? {
var inputString : String? = xml.childOrNull(property.name)?.value()?.replace("_","0")
if (inputString?.length == 16) inputString = inputString.plus(":00")
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)*/ } } }
}
open class DateTimeXmlDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATETIME_FORMAT) {
private val format = dateTimeFormat(formatString)
//FB TODO ** To rewrite
//open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDateTime? = xml.childOrNull(property.name)?.value()?.let { LocalDateTime.parse(it, format) }
open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDateTime {
var inputString : String? = xml.childOrNull(property.name)?.value()?.replace("_","0")
if (inputString?.length == 16) inputString = inputString.plus(":00")
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)*/ } } }
}
fun <F: XmlFormat, T: Any> KClass<F>.instantiate(content: T): F = constructors.first().call(content)
open class ObjectXmlDelegate <T: XmlFormat> (val xml: Node, private val klass: KClass<T>) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
val obj = xml.element().child(property.name)
return obj.let {
klass.instantiate(it)
}
}
}
// standard types attributes delegates
open class OptionalStringXmlAttrDelegate(val xml: Element) {
open operator fun getValue(thisRef: Any?, property: KProperty<*>): String? = xml.attr(property.name)
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) { value?.let { xml.setAttr(property.name, value) } }
}
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 class OptionalBooleanXmlAttrDelegate(val xml: Element) {
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Boolean? = xml.boolAttr(property.name)
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean?) { value?.let { xml.setAttr(property.name, value) } }
}
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 class OptionalIntXmlAttrDelegate(val xml: Element) {
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Int? = xml.intAttr(property.name)
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int?) { value?.let { xml.setAttr(property.name, value) } }
}
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 class OptionalLongXmlAttrDelegate(val xml: Element) {
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Long? = xml.longAttr(property.name)
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Long?) { value?.let { xml.setAttr(property.name, value) } }
}
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 class OptionalDoubleXmlAttrDelegate(val xml: Element) {
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Double? = xml.doubleAttr(property.name)
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Double?) { value?.let { xml.setAttr(property.name, value) } }
}
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 class OptionalDateXmlAttrDelegate(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*/) }
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDate?) { value?.let { xml.setAttr(property.name, value.toString() /* format.format(value) */) } }
}
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) */) } }
}
// containers delegates
open class XmlArrayAdapter<T: XmlFormat>(val parent: Node, protected val functor: (Node)->T): List<T> {
override val size = (parent as ElementTraversal).childElementCount
override fun contains(element: T): Boolean { throw Error("not implemented") }
override fun containsAll(elements: Collection<T>): Boolean { throw Error("not implemented") }
override fun get(index: Int): T {
try {
return functor(parent.element().children()[index])
} catch (e: Exception) {
throw Error("could not get child element", e)
}
}
override fun indexOf(element: T): Int { throw Error("not implemented") }
override fun isEmpty() = parent.childNodes.length == 0
override fun iterator(): Iterator<T> = object: Iterator<T> {
private val it = parent.element().children().iterator()
override fun hasNext() = it.hasNext()
override fun next() = functor(it.next())
}
override fun lastIndexOf(element: T): Int { throw Error("not implemented") }
override fun listIterator() = listIterator(0)
override fun listIterator(index: Int): ListIterator<T> { throw Error("not implemented") }
override fun subList(fromIndex: Int, toIndex: Int): List<T> { throw Error("not implemented") }
}
class MutableXmlArrayAdapter<T: XmlFormat>(parent: Node, functor: (Node)->T): XmlArrayAdapter<T>(parent, functor), MutableList<T> {
override fun iterator(): MutableIterator<T> = object: MutableIterator<T> {
private val it = parent.element().children().iterator()
override fun hasNext() = it.hasNext()
override fun next() = functor(it.next())
override fun remove() { throw Error("not implemented") }
}
override fun add(element: T): Boolean { parent.appendChild(element.xml); return true }
override fun add(index: Int, element: T) { throw Error("not implemented") }
override fun addAll(index: Int, elements: Collection<T>): Boolean { throw Error("not implemented") }
override fun addAll(elements: Collection<T>): Boolean { throw Error("not implemented") }
override fun clear() {
while (parent.firstChild != null) {
parent.removeChild(parent.firstChild)
}
}
override fun listIterator() = listIterator(0)
override fun listIterator(index: Int): MutableListIterator<T> { throw Error("not implemented") }
override fun remove(element: T): Boolean { throw Error("not implemented") }
override fun removeAll(elements: Collection<T>): Boolean { throw Error("not implemented") }
override fun removeAt(index: Int): T { throw Error("not implemented") }
override fun retainAll(elements: Collection<T>): Boolean { throw Error("not implemented") }
override fun set(index: Int, element: T): T { throw Error("not implemented") }
override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> {
throw NotImplementedError("Not implemented")
}
}
open class XmlArrayInlineAdapter<T: XmlFormat>(val list: NodeList, protected val functor: (Node)->T): List<T> {
override val size = list.length
override fun contains(element: T): Boolean { throw Error("not implemented") }
override fun containsAll(elements: Collection<T>): Boolean { throw Error("not implemented") }
override fun get(index: Int): T {
try {
return functor(list[index])
} catch (e: Exception) {
throw Error("could not get child element", e)
}
}
override fun indexOf(element: T): Int { throw Error("not implemented") }
override fun isEmpty() = list.length == 0
override fun iterator(): Iterator<T> = object: Iterator<T> {
private val it = list.iterator()
override fun hasNext() = it.hasNext()
override fun next() = functor(it.next())
}
override fun lastIndexOf(element: T): Int { throw Error("not implemented") }
override fun listIterator() = listIterator(0)
override fun listIterator(index: Int): ListIterator<T> { throw Error("not implemented") }
override fun subList(fromIndex: Int, toIndex: Int): List<T> { throw Error("not implemented") }
}
inline fun <reified T: XmlFormat> MutableXmlArrayAdapter<T>.newChild(): T {
val node = parent.document().createElement(T::class.simpleName!!.lowercase())
// should be done explicitely in client code
// parent.element().appendChild(node)
return T::class.instantiate(node)
}
open class ArrayXmlDelegate <T: XmlFormat> (val xml: Node, private val klass: KClass<T>) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): List<T> =
XmlArrayAdapter<T>(xml.element().child(property.name), klass::instantiate)
}
open class MutableArrayXmlDelegate <T: XmlFormat> (val xml: Node, private val klass: KClass<T>) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): MutableXmlArrayAdapter<T> =
MutableXmlArrayAdapter<T>(xml.element().child(property.name), klass::instantiate)
}
open class ChildrenArrayXmlDelegate <T: XmlFormat> (val xml: Node, private val tagName: String, private val klass: KClass<T>) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): List<T> = XmlArrayInlineAdapter(xml.element().getElementsByTagName(tagName), klass::instantiate)
}

View File

@@ -0,0 +1,556 @@
package org.jeudego.pairgoth.util
import org.apache.commons.lang3.StringEscapeUtils
import org.slf4j.LoggerFactory
import org.w3c.dom.*
import org.xml.sax.ErrorHandler
import org.xml.sax.InputSource
import org.xml.sax.SAXException
import org.xml.sax.SAXParseException
import java.io.Reader
import java.io.StringReader
import java.io.StringWriter
import java.lang.ref.SoftReference
import java.nio.charset.Charset
import java.util.*
import java.util.concurrent.LinkedBlockingDeque
import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilder
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerException
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathExpressionException
import javax.xml.xpath.XPathFactory
/**
*
* Utility class for simplifying parsing of xml documents. Documents are not validated, and
* loading of external files (xinclude, external entities, DTDs, etc.) are disabled.
*
* @author Claude Brisson
*/
object XmlUtils {
/* several pieces of code were borrowed from the Apache Shindig XmlUtil class.*/
private val LOGGER = LoggerFactory.getLogger(XmlUtils::class.java)
/**
* Handles xml errors so that they're not logged to stderr.
*/
private val errorHandler: ErrorHandler = object : ErrorHandler {
@Throws(SAXException::class)
override fun error(exception: SAXParseException) {
throw exception
}
@Throws(SAXException::class)
override fun fatalError(exception: SAXParseException) {
throw exception
}
override fun warning(exception: SAXParseException) {
LOGGER.info("warning during parsing", exception)
}
}
private var canReuseBuilders = false
private val builderFactory = createDocumentBuilderFactory()
private fun createDocumentBuilderFactory(): DocumentBuilderFactory {
val builderFactory = DocumentBuilderFactory.newInstance()
// Namespace support is required for <os:> elements
builderFactory.isNamespaceAware = true
// Disable various insecure and/or expensive options.
builderFactory.isValidating = false
// Can't disable doctypes entirely because they're usually harmless. External entity
// resolution, however, is both expensive and insecure.
try {
builderFactory.setAttribute("http://xml.org/sax/features/external-general-entities", false)
} catch (e: IllegalArgumentException) {
// Not supported by some very old parsers.
LOGGER.info("Error parsing external general entities: ", e)
}
try {
builderFactory.setAttribute("http://xml.org/sax/features/external-parameter-entities", false)
} catch (e: IllegalArgumentException) {
// Not supported by some very old parsers.
LOGGER.info("Error parsing external parameter entities: ", e)
}
try {
builderFactory.setAttribute("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
} catch (e: IllegalArgumentException) {
// Only supported by Apache's XML parsers.
LOGGER.info("Error parsing external DTD: ", e)
}
try {
builderFactory.setAttribute(XMLConstants.FEATURE_SECURE_PROCESSING, true)
} catch (e: IllegalArgumentException) {
// Not supported by older parsers.
LOGGER.info("Error parsing secure XML: ", e)
}
return builderFactory
}
private val reusableBuilder: ThreadLocal<DocumentBuilder> = object : ThreadLocal<DocumentBuilder>() {
override fun initialValue(): DocumentBuilder {
return try {
LOGGER.trace("Created a new document builder")
builderFactory.newDocumentBuilder()
} catch (e: ParserConfigurationException) {
throw Error(e)
}
}
}
init {
try {
val builder = builderFactory.newDocumentBuilder()
builder.reset()
canReuseBuilders = true
LOGGER.trace("reusing document builders")
} catch (e: UnsupportedOperationException) {
// Only supported by newer parsers (xerces 2.8.x+ for instance).
canReuseBuilders = false
LOGGER.trace("not reusing document builders")
} catch (e: ParserConfigurationException) {
// Only supported by newer parsers (xerces 2.8.x+ for instance).
canReuseBuilders = false
LOGGER.trace("not reusing document builders")
}
}
private val builderPool = LinkedBlockingDeque<SoftReference<DocumentBuilder?>>() // contains only idle builders
private val maxBuildersCount = 100
private var currentBuildersCount = 0
/**
* Get a document builder
* @return document builder
*/
@Synchronized
private fun getDocumentBuilder(): DocumentBuilder {
var builder: DocumentBuilder? = null
if (canReuseBuilders && builderPool.size > 0) {
builder = builderPool.pollFirst().get()
}
if (builder == null) {
if (!canReuseBuilders || currentBuildersCount < maxBuildersCount) {
try {
builder = builderFactory.newDocumentBuilder()
builder.setErrorHandler(errorHandler)
++currentBuildersCount
} catch (e: Exception) {
/* this is a fatal error */
throw Error("could not create a new XML DocumentBuilder instance", e)
}
} else {
try {
LOGGER.warn(
"reached XML DocumentBuilder pool size limit, current thread needs to wait",
)
builder = builderPool.takeFirst().get()
} catch (ie: InterruptedException) {
LOGGER.warn("caught an InterruptedException while waiting for a DocumentBuilder instance")
}
}
}
return builder ?: throw Error("could not create a new XML DocumentBuilder instance")
}
/**
* Release the given document builder
* @param builder document builder
*/
@Synchronized
private fun releaseBuilder(builder: DocumentBuilder?) {
builder!!.reset()
builderPool.addLast(SoftReference(builder))
}
/**
* Creates an empty document
*/
fun createDocument(): Document {
val builder = getDocumentBuilder()
val doc = builder.newDocument()
releaseBuilder(builder)
return doc
}
/**
* Extracts an attribute from a node.
*
* @param node target node
* @param attr attribute name
* @param def default value
* @return The value of the attribute, or def
*/
fun getAttribute(node: Node, attr: String?, def: String?): String? {
val attrs = node.attributes
val `val` = attrs.getNamedItem(attr)
return if (`val` != null) {
`val`.nodeValue
} else def
}
/**
* @param node target node
* @param attr attribute name
* @return The value of the given attribute, or null if not present.
*/
fun getAttribute(node: Node, attr: String?): String? {
return getAttribute(node, attr, null)
}
/**
* Retrieves an attribute as a boolean.
*
* @param node target node
* @param attr attribute name
* @param def default value
* @return True if the attribute exists and is not equal to "false"
* false if equal to "false", and def if not present.
*/
fun getBoolAttribute(node: Node, attr: String?, def: Boolean): Boolean {
val value = getAttribute(node, attr) ?: return def
return java.lang.Boolean.parseBoolean(value)
}
/**
* @param node target node
* @param attr attribute name
* @return True if the attribute exists and is not equal to "false"
* false otherwise.
*/
fun getBoolAttribute(node: Node, attr: String?): Boolean {
return getBoolAttribute(node, attr, false)
}
/**
* @param node target node
* @param attr attribute name
* @param def default value
* @return An attribute coerced to an integer.
*/
fun getIntAttribute(node: Node, attr: String?, def: Int): Int {
val value = getAttribute(node, attr) ?: return def
return try {
value.toInt()
} catch (e: NumberFormatException) {
def
}
}
/**
* @param node target node
* @param attr attribute name
* @return An attribute coerced to an integer.
*/
fun getIntAttribute(node: Node, attr: String?): Int {
return getIntAttribute(node, attr, 0)
}
/**
* Attempts to parse the input xml into a single element.
* @param xml xml stream reader
* @return The document object
*/
fun parse(xml: Reader): Element {
val builder = getDocumentBuilder()
try {
val doc = builder.parse(InputSource(xml))
return doc.documentElement
} finally {
releaseBuilder(builder)
}
}
/**
* Attempts to parse the input xml into a single element.
* @param xml xml string
* @return The document object
*/
fun parse(xml: String): Element = parse(StringReader(xml))
/**
* Search for nodes using an XPath expression
* @param xpath XPath expression
* @param context evaluation context
* @return org.w3c.NodeList of found nodes
* @throws XPathExpressionException
*/
@Throws(XPathExpressionException::class)
fun search(xpath: String?, context: Node?): NodeList {
val xp = XPathFactory.newInstance().newXPath()
val exp = xp.compile(xpath)
return exp.evaluate(context, XPathConstants.NODESET) as NodeList
}
/**
* Search for nodes using an XPath expression
* @param xpath XPath expression
* @param context evaluation context
* @return List of found nodes
* @throws XPathExpressionException
*/
@Throws(XPathExpressionException::class)
fun getNodes(xpath: String?, context: Node?): List<Node> {
val ret: MutableList<Node> = ArrayList()
val lst = search(xpath, context)
for (i in 0 until lst.length) {
ret.add(lst.item(i))
}
return ret
}
/**
* Search for elements using an XPath expression
* @param xpath XPath expression
* @param context evaluation context
* @return List of found elements
* @throws XPathExpressionException
*/
@Throws(XPathExpressionException::class)
fun getElements(xpath: String?, context: Node?): List<Element> {
val ret: MutableList<Element> = ArrayList()
val lst = search(xpath, context)
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)
}
return ret
}
/**
*
* Builds the xpath expression for a node (tries to use id/name nodes when possible to get a unique path)
* @param n target node
* @return node xpath
*/
// (borrow from http://stackoverflow.com/questions/5046174/get-xpath-from-the-org-w3c-dom-node )
fun nodePath(n: Node): String {
// declarations
var parent: Node?
val hierarchy = Stack<Node>()
val buffer = StringBuffer("/")
// push element on stack
hierarchy.push(n)
parent = when (n.nodeType) {
Node.ATTRIBUTE_NODE -> (n as Attr).ownerElement
Node.COMMENT_NODE, Node.ELEMENT_NODE, Node.DOCUMENT_NODE -> n.parentNode
else -> throw IllegalStateException("Unexpected Node type" + n.nodeType)
}
while (null != parent && parent.nodeType != Node.DOCUMENT_NODE) {
// push on stack
hierarchy.push(parent)
// get parent of parent
parent = parent.parentNode
}
// construct xpath
var obj: Any? = null
while (!hierarchy.isEmpty() && null != hierarchy.pop().also { obj = it }) {
val node = obj as Node?
var handled = false
if (node!!.nodeType == Node.ELEMENT_NODE) {
val e = node as Element?
// is this the root element?
if (buffer.length == 1) {
// root element - simply append element name
buffer.append(node.nodeName)
} else {
// child element - append slash and element name
buffer.append("/")
buffer.append(node.nodeName)
if (node.hasAttributes()) {
// see if the element has a name or id attribute
if (e!!.hasAttribute("id")) {
// id attribute found - use that
buffer.append("[@id='" + e.getAttribute("id") + "']")
handled = true
} else if (e.hasAttribute("name")) {
// name attribute found - use that
buffer.append("[@name='" + e.getAttribute("name") + "']")
handled = true
}
}
if (!handled) {
// no known attribute we could use - get sibling index
var prev_siblings = 1
var prev_sibling = node.previousSibling
while (null != prev_sibling) {
if (prev_sibling.nodeType == node.nodeType) {
if (prev_sibling.nodeName.equals(
node.nodeName, ignoreCase = true
)
) {
prev_siblings++
}
}
prev_sibling = prev_sibling.previousSibling
}
buffer.append("[$prev_siblings]")
}
}
} else if (node.nodeType == Node.ATTRIBUTE_NODE) {
buffer.append("/@")
buffer.append(node.nodeName)
}
}
// return buffer
return buffer.toString()
}
/**
* XML Node to string
* @param node XML node
* @return XML node string representation
*/
fun nodeToString(node: Node?, encoding: Charset = Charsets.UTF_8): String {
val sw = StringWriter()
try {
val t = TransformerFactory.newInstance().newTransformer()
t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no")
t.setOutputProperty(OutputKeys.INDENT, "no")
t.setOutputProperty(OutputKeys.ENCODING, encoding.name())
t.transform(DOMSource(node), StreamResult(sw))
} catch (te: TransformerException) {
LOGGER.error("could not convert XML node to string", te)
}
return sw.toString()
}
/**
* XML Node to string
* @param node XML node
* @return XML node string representation
*/
fun nodeToPrettyString(node: Node, encoding: Charset = Charsets.UTF_8): String {
val sw = StringWriter()
try {
val t = TransformerFactory.newInstance().newTransformer()
t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no")
t.setOutputProperty(OutputKeys.INDENT, "yes")
t.setOutputProperty(OutputKeys.ENCODING, encoding.name())
t.transform(DOMSource(node), StreamResult(sw))
} catch (te: TransformerException) {
LOGGER.error("could not convert XML node to string", te)
}
return sw.toString()
}
/**
* Checkes whether the given mime type is an XML format
* @param mimeType mime type
* @return `true` if this mime type is an XML format
*/
fun isXmlMimeType(mimeType: String?): Boolean {
return mimeType != null &&
("text/xml" == mimeType || "application/xml" == mimeType ||
mimeType.endsWith("+xml"))
}
}
// utility extension functions
fun emptyDocument(root: String) = XmlUtils.createDocument().also { it.appendChild(it.createElement(root)) }
fun Node.element(): Element =
when (this) {
is Element -> this
is Document -> documentElement
else -> throw Error("invalid xml node")
}
fun Element.children(): List<Element> {
val ret = mutableListOf<Element>()
for (i in 0..childNodes.length) {
val child = childNodes[i]
if (child is Element) ret.add(child)
}
return ret
}
fun Node.document(): Document = ownerDocument ?: this as Document
fun Element.childOrNull(key: String): Element? = children().firstOrNull { it.tagName == key }
fun Element.child(key: String): Element = childOrNull(key) ?: addChild(key)
fun Node.addChild(tag: String): Element = appendChild(document().createElement(tag)) as Element
fun Node.value(): String? = textContent.let { if (it.isEmpty()) null else it }
fun Node.attr(key: String, def: String? = null) = attributes.getNamedItem(key)?.nodeValue ?: def
fun Element.setAttr(key: String, value: Any) {
setAttribute(key, value.toString())
}
fun Node.boolAttr(key: String, def: Boolean? = null) = attr(key)?.toBoolean() ?: def
fun Node.intAttr(key: String, def: Int? = null) = attr(key)?.toInt() ?: def
fun Node.longAttr(key: String, def: Long? = null) = attr(key)?.toLong() ?: def
fun Node.doubleAttr(key: String, def: Double? = null) = attr(key)?.toDouble() ?: def
fun Node.path() = XmlUtils.nodePath(this)
fun Node.find(xpath: String): NodeList {
return XPathFactory.newInstance().newXPath().compile(xpath).evaluate(this, XPathConstants.NODESET) as NodeList
}
fun Node.print(encoding: Charset = Charsets.UTF_8) : String {
trimTextNodes()
return XmlUtils.nodeToString(this, encoding)
/* previous implementation, without charset
val domImplLS = document().implementation as DOMImplementationLS
val serializer = domImplLS.createLSSerializer()
return serializer.writeToString(this)
*/
}
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) {
child.textContent = child.textContent.trim()
}
else child.trimTextNodes()
}
}
fun Node.prettyPrint(encoding: Charset = Charsets.UTF_8): String {
trimTextNodes()
return XmlUtils.nodeToPrettyString(this, encoding)
}
// node list iteration and random access
class NodeListIterator(private val lst: NodeList): Iterator<Node> {
private var nextPos = 0
override fun hasNext() = nextPos < lst.length
override fun next() = lst.item(nextPos++)
}
operator fun NodeList.iterator() = NodeListIterator(this)
operator fun NodeList.get(i: Int) = item(i)
// Encode XML entities in a string
fun String.encodeXmlEntities() = StringEscapeUtils.escapeXml(this)

View File

@@ -0,0 +1,36 @@
package org.jeudego.pairgoth.web
import com.republicate.kson.Json
import java.io.IOException
class ApiException : IOException {
var code: Int
private set
var details: Json.Object
private set
constructor(code: Int) : super("error") {
this.code = code
details = Json.Object("message" to message)
}
constructor(code: Int, message: String?) : super(message) {
this.code = code
details = Json.Object("message" to message)
}
constructor(code: Int, cause: Exception) : super(cause) {
this.code = code
details = Json.Object("message" to "Erreur interne du serveur : " + cause.message)
}
constructor(code: Int, message: String, cause: Exception) : super(message, cause) {
this.code = code
details = Json.Object("message" to message + " : " + cause.message)
}
constructor(code: Int, details: Json.Object) : super(details.getString("message")) {
this.code = code
this.details = details
}
}

View File

@@ -0,0 +1,268 @@
package org.jeudego.pairgoth.web
import com.republicate.kson.Json
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
import org.jeudego.pairgoth.util.Colorizer.red
import org.jeudego.pairgoth.util.XmlUtils
import org.jeudego.pairgoth.util.parse
import org.jeudego.pairgoth.util.toString
import org.slf4j.LoggerFactory
import org.w3c.dom.Element
import java.io.IOException
import java.util.*
import java.util.concurrent.locks.ReadWriteLock
import java.util.concurrent.locks.ReentrantReadWriteLock
import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
class ApiServlet : HttpServlet() {
public override fun doGet(request: HttpServletRequest, response: HttpServletResponse) {
doRequest(request, response)
}
public override fun doPost(request: HttpServletRequest, response: HttpServletResponse) {
doRequest(request, response)
}
public override fun doPut(request: HttpServletRequest, response: HttpServletResponse) {
doRequest(request, response)
}
public override fun doDelete(request: HttpServletRequest, response: HttpServletResponse) {
doRequest(request, response)
}
private val lock: ReadWriteLock = ReentrantReadWriteLock()
private fun doRequest(request: HttpServletRequest, response: HttpServletResponse) {
val requestLock = if (request.method == "GET") lock.readLock() else lock.writeLock()
try {
requestLock.lock()
doProtectedRequest(request, response)
} finally {
requestLock.unlock()
}
}
private fun doProtectedRequest(request: HttpServletRequest, response: HttpServletResponse) {
val uri = request.requestURI
logger.logRequest(request, !uri.contains(".") && uri.length > 1)
var payload: Json? = null
var reason = "OK"
try {
// validate request
if ("dev" == WebappManager.getProperty("webapp.env")) {
response.addHeader("Access-Control-Allow-Origin", "*")
}
validateAccept(request);
validateContentType(request)
// parse request URI
val parts = uri.split("/").filter { !it.isEmpty() }
if (parts.size !in 2..5 || parts[0] != "api") throw ApiException(HttpServletResponse.SC_BAD_REQUEST)
val entity = parts[1]
val selector = parts.getOrNull(2)?.also { request.setAttribute(ApiHandler.SELECTOR_KEY, it) }
val subEntity = parts.getOrNull(3)
val subSelector = parts.getOrNull(4)?.also { request.setAttribute(ApiHandler.SUBSELECTOR_KEY, it) }
// choose handler
val handler = when (entity) {
"tour" ->
when (subEntity) {
null -> TournamentHandler
"part" -> PlayerHandler
"pair" -> PairingHandler
"res" -> ResultsHandler
"team" -> TeamHandler
else -> ApiHandler.badRequest("unknown sub-entity: $subEntity")
}
"player" -> PlayerHandler
else -> ApiHandler.badRequest("unknown entity: $entity")
}
// call handler
payload = handler.route(request, response)
// if payload is null, it means the handler already sent the response
if (payload != null) {
setContentType(response)
payload.toString(response.writer)
}
} catch (apiException: ApiException) {
reason = apiException.message ?: "unknown API error"
error(
request,
response,
apiException.code,
reason,
apiException
)
} catch (ioe: IOException) {
logger.error(red("could not process call"), ioe)
reason = ioe.message ?: "unknown i/o exception"
error(request, response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, reason, ioe)
} finally {
val builder = StringBuilder()
builder.append(response.status).append(' ')
.append(reason)
if (response.status == HttpServletResponse.SC_INTERNAL_SERVER_ERROR) {
logger.trace(red(">> {}"), builder.toString())
} else {
logger.trace(green(">> {}"), builder.toString())
}
// CB TODO - should be bufferized and asynchronously written in synchronous chunks
// so that header lines from parallel requests are not mixed up in the logs ;
// synchronizing the whole request log is not desirable
for (header in response.headerNames) {
val value = response.getHeader(header)
logger.trace(green(">> {}: {}"), header, value)
}
if (payload != null) {
try {
logger.logPayload(">> ", payload, false)
} catch (ioe: IOException) {
}
}
}
}
@Throws(ApiException::class)
protected fun validateContentType(request: HttpServletRequest) {
// extract content type parts
val contentType = request.contentType
if (contentType == null) {
if (request.method == "GET") return
throw ApiException(
HttpServletResponse.SC_BAD_REQUEST,
"no content type header"
)
}
val sep = contentType.indexOf(';')
val mimeType: String
var charset: String? = null
if (sep == -1) mimeType = contentType else {
mimeType = contentType.substring(0, sep).trim { it <= ' ' }
val params =
contentType.substring(sep + 1).split("=".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
if (params.size == 2 && params[0].lowercase(Locale.getDefault())
.trim { it <= ' ' } == "charset"
) {
charset = params[1].lowercase(Locale.getDefault()).trim { it <= ' ' }
.replace("-".toRegex(), "")
}
}
// check charset
if (charset != null && EXPECTED_CHARSET != charset) throw ApiException(
HttpServletResponse.SC_BAD_REQUEST,
"UTF-8 content expected"
)
// check content type
if (isJson(mimeType)) {
// put Json body as request attribute
try {
Json.parse(request.reader)?.let { payload: Json ->
request.setAttribute(ApiHandler.PAYLOAD_KEY, payload)
if (logger.isInfoEnabled) {
logger.logPayload("<< ", payload, true)
}
}
} catch (ioe: IOException) {
throw ApiException(HttpServletResponse.SC_BAD_REQUEST, ioe)
}
} else if (isXml(mimeType)) {
// 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 ->
request.setAttribute(ApiHandler.PAYLOAD_KEY, payload)
logger.info(blue("<< (xml document)"))
}
} catch(ioe: IOException) {
throw ApiException(HttpServletResponse.SC_BAD_REQUEST, ioe)
}
}
else throw ApiException(
HttpServletResponse.SC_BAD_REQUEST,
"JSON content expected"
)
}
@Throws(ApiException::class)
protected fun validateAccept(request: HttpServletRequest) {
val accept = request.getHeader("Accept")
?: throw ApiException(
HttpServletResponse.SC_BAD_REQUEST,
"Missing 'Accept' header"
)
if (!isJson(accept) && (!isXml(accept) || !request.requestURI.matches(Regex("/api/tour/\\d+")))) throw ApiException(
HttpServletResponse.SC_BAD_REQUEST,
"Invalid 'Accept' header"
)
}
protected fun setContentType(response: HttpServletResponse) {
response.contentType = "application/json; charset=UTF-8"
}
protected fun error(
request: HttpServletRequest,
response: HttpServletResponse,
code: Int,
message: String?,
cause: Throwable? = null
) {
try {
if (code == 500) {
logger.error(
"Request {} {} gave error {} {}",
request.method,
request.requestURI,
code,
message,
cause
)
}
response.sendError(code, message)
} catch (ioe: IOException) {
logger.error("Could not send back error", ioe)
}
}
protected fun error(response: HttpServletResponse, code: Int) {
try {
response.sendError(code)
} catch (ioe: IOException) {
logger.error("Could not send back error", ioe)
}
}
companion object {
private var logger = LoggerFactory.getLogger("api")
private const val EXPECTED_CHARSET = "utf8"
const val AUTH_HEADER = "Authorization"
const val AUTH_PREFIX = "Bearer"
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

@@ -0,0 +1,34 @@
package org.jeudego.pairgoth.web
import info.macias.sse.events.MessageEvent
import java.util.concurrent.atomic.AtomicLong
enum class Event {
tournamentAdded,
tournamentUpdated,
tournamentDeleted,
playerAdded,
playerUpdated,
playerDeleted,
teamAdded,
teamUpdated,
teamDeleted,
gamesAdded,
gamesDeleted,
gameUpdated,
resultUpdated,
;
companion object {
private val nextMessageId = AtomicLong(0)
private val sse: SSEServlet by lazy { SSEServlet.getInstance() }
private fun <T> buildEvent(event: Event, data: T) = MessageEvent.Builder()
.setId("${nextMessageId.incrementAndGet()}".padStart(10, '0'))
.setEvent(event.name)
.setData(data.toString())
.build()
internal fun <T> dispatch(event: Event, data: T) {
sse.broadcast(buildEvent(event, data))
}
}
}

View File

@@ -0,0 +1,71 @@
package org.jeudego.pairgoth.web
import com.republicate.kson.Json
import org.jeudego.pairgoth.util.Colorizer.blue
import org.jeudego.pairgoth.util.Colorizer.green
import org.jeudego.pairgoth.util.toString
import org.slf4j.Logger
import java.io.StringWriter
import javax.servlet.http.HttpServletRequest
fun Logger.logRequest(req: HttpServletRequest, logHeaders: Boolean = false) {
val builder = StringBuilder()
builder.append(req.method).append(' ')
.append(req.scheme).append("://")
.append(req.localName)
val port = req.localPort
if (port != 80) builder.append(':').append(port)
if (!req.contextPath.isEmpty()) {
builder.append(req.contextPath)
}
builder.append(req.requestURI)
if (req.method == "GET") {
val qs = req.queryString
if (qs != null) builder.append('?').append(qs)
}
// builder.append(' ').append(req.getProtocol());
info(blue("<< {}"), builder.toString())
if (isTraceEnabled && logHeaders) {
// CB TODO - should be bufferized and asynchronously written in synchronous chunks
// so that header lines from parallel requests are not mixed up in the logs ;
// synchronizing the whole request log is not desirable
val headerNames = req.headerNames
while (headerNames.hasMoreElements()) {
val name = headerNames.nextElement()
val value = req.getHeader(name)
trace(blue("<< {}: {}"), name, value)
}
}
}
fun Logger.logPayload(prefix: String?, payload: Json, upstream: Boolean) {
val writer = StringWriter()
//payload.toPrettyString(writer, "");
payload.toString(writer)
if (isTraceEnabled) {
for (line in writer.toString().split("\n".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()) {
trace(if (upstream) blue("{}{}") else green("{}{}"), prefix, line)
}
} else {
var line = writer.toString()
val pos = line.indexOf('\n')
if (pos != -1) line = line.substring(0, pos)
if (line.length > 50) line = line.substring(0, 50) + "..."
debug(if (upstream) blue("{}{}") else green("{}{}"), prefix, line)
}
}
fun HttpServletRequest.getRemoteAddress(): String? {
var ip = getHeader("X-Forwarded-For")
if (ip == null) {
ip = remoteAddr
} else {
val comma = ip.indexOf(',')
if (comma != -1) {
ip = ip.substring(0, comma).trim { it <= ' ' } // keep the left-most IP address
}
}
return ip
}

View File

@@ -0,0 +1,30 @@
package org.jeudego.pairgoth.web
import info.macias.sse.EventBroadcast
import info.macias.sse.events.MessageEvent
import info.macias.sse.servlet3.ServletEventTarget
import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
class SSEServlet: HttpServlet() {
companion object {
private val logger = LoggerFactory.getLogger("sse")
private var zeInstance: SSEServlet? = null
internal fun getInstance(): SSEServlet = zeInstance ?: throw Error("SSE servlet not ready")
}
init {
if (zeInstance != null) throw Error("Multiple instances of SSE servlet found!")
zeInstance = this
}
private val broadcast = EventBroadcast()
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse?) {
logger.trace("<< new channel")
broadcast.addSubscriber(ServletEventTarget(req), req.getHeader("Last-Event-Id"))
}
internal fun broadcast(message: MessageEvent) = broadcast.broadcast(message)
}

View File

@@ -0,0 +1,167 @@
package org.jeudego.pairgoth.web
import com.republicate.mailer.SmtpLoop
import org.apache.commons.lang3.tuple.Pair
import org.slf4j.LoggerFactory
import java.io.IOException
import java.lang.IllegalAccessError
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.util.*
import java.util.IllegalFormatCodePointException
import javax.net.ssl.*
import javax.servlet.*
import javax.servlet.annotation.WebListener
import javax.servlet.http.HttpSessionEvent
import javax.servlet.http.HttpSessionListener
@WebListener
class WebappManager : ServletContextListener, ServletContextAttributeListener, HttpSessionListener {
private fun disableSSLCertificateChecks() {
// see http://www.nakov.com/blog/2009/07/16/disable-certificate-validation-in-java-ssl-connections/
try {
// Create a trust manager that does not validate certificate chains
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate>? {
return null
}
@Suppress("TrustAllX509TrustManager")
override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) {}
@Suppress("TrustAllX509TrustManager")
override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) {}
}
)
// Install the all-trusting trust manager
val sc = SSLContext.getInstance("SSL")
sc.init(null, trustAllCerts, SecureRandom())
HttpsURLConnection.setDefaultSSLSocketFactory(sc.socketFactory)
// Create all-trusting host name verifier
val allHostsValid = HostnameVerifier { hostname, session -> true }
// Install the all-trusting host verifier
HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid)
} catch (e: Exception) {
logger.error("could not disable SSL certificate checks", e)
}
}
/* ServletContextListener interface */
override fun contextInitialized(sce: ServletContextEvent) {
context = sce.servletContext
logger.info("---------- Starting Web Application ----------")
context.setAttribute("manager", this)
webappRoot = context.getRealPath("/")
try {
// load default properties
properties.load(context.getResourceAsStream("/WEB-INF/pairgoth.default.properties"))
// override with system properties after stripping off the 'pairgoth.' prefix
System.getProperties().filter { (key, value) -> key is String && key.startsWith(PAIRGOTH_PROPERTIES_PREFIX)
}.forEach { (key, value) ->
properties[(key as String).removePrefix(PAIRGOTH_PROPERTIES_PREFIX)] = value
}
logger.info("Using profile {}", properties.getProperty("webapp.env"))
// set system user agent string to empty string
System.setProperty("http.agent", "")
// disable (for now ?) the SSL certificate checks, because many sites
// fail to correctly implement SSL...
disableSSLCertificateChecks()
// start smtp loop
if (properties.containsKey("smtp.host")) {
registerService("smtp", SmtpLoop(properties))
startService("smtp")
}
} catch (ioe: IOException) {
logger.error("webapp initialization error", ioe)
}
}
override fun contextDestroyed(sce: ServletContextEvent) {
logger.info("---------- Stopping Web Application ----------")
val context = sce.servletContext
for (service in webServices.keys) stopService(service, true)
// ??? DriverManager.deregisterDriver(com.mysql.cj.jdbc.Driver ...);
}
/* ServletContextAttributeListener interface */
override fun attributeAdded(event: ServletContextAttributeEvent) {}
override fun attributeRemoved(event: ServletContextAttributeEvent) {}
override fun attributeReplaced(event: ServletContextAttributeEvent) {}
/* HttpSessionListener interface */
override fun sessionCreated(se: HttpSessionEvent) {}
override fun sessionDestroyed(se: HttpSessionEvent) {}
companion object {
const val PAIRGOTH_PROPERTIES_PREFIX = "pairgoth."
lateinit var webappRoot: String
lateinit var context: ServletContext
private val webServices: MutableMap<String?, Pair<Runnable, Thread?>> = TreeMap()
var logger = LoggerFactory.getLogger(WebappManager::class.java)
val properties = Properties()
fun getProperty(prop: String): String? {
return properties.getProperty(prop)
}
fun getMandatoryProperty(prop: String): String {
return properties.getProperty(prop) ?: throw Error("missing property: ${prop}")
}
val webappURL by lazy { getProperty("webapp.url") }
private val services = mutableMapOf<String, Pair<Runnable, Thread>>()
@JvmOverloads
fun registerService(name: String?, task: Runnable, initialStatus: Boolean? = null) {
if (webServices.containsKey(name)) {
logger.warn("service {} already registered")
return
}
logger.debug("registered service {}", name)
webServices[name] =
Pair.of(task, null)
}
fun startService(name: String?) {
val service = webServices[name]!!
if (service.right != null && service.right!!.isAlive) {
logger.warn("service {} is already running", name)
return
}
logger.debug("starting service {}", name)
val thread = Thread(service.left, name)
thread.start()
webServices[name] =
Pair.of(
service.left,
thread
)
}
@JvmOverloads
fun stopService(name: String?, webappClosing: Boolean = false) {
val service = webServices[name]!!
val thread = service.right
if (thread == null || !thread.isAlive) {
logger.warn("service {} is already stopped", name)
return
}
logger.debug("stopping service {}", name)
thread.interrupt()
try {
thread.join()
} catch (ie: InterruptedException) {
}
if (!webappClosing) {
webServices[name] = Pair.of(service.left, null)
}
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
<!-- https://www.eclipse.org/jetty/documentation/jetty-9/index.html#file-alias-serving -->
<Call name="addAliasCheck">
<Arg>
<New class="org.eclipse.jetty.server.handler.AllowSymLinkAliasChecker" />
</Arg>
</Call>
</Configure>

View File

@@ -0,0 +1,18 @@
# webapp
webapp.env = dev
webapp.url = http://localhost:8080
# store
store = file
store.file.path = tournamentfiles
# smtp
smtp.sender =
smtp.host =
smtp.port = 587
smtp.user =
smtp.password =
# logging
logger.level = trace
logger.format = [%level] %ip [%logger] %message

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!-- Listeners -->
<!-- we're not using @WebListener annotations so that our manager is initialized *after* the webapp logger -->
<listener>
<listener-class>com.republicate.slf4j.impl.ServletContextLoggerListener</listener-class>
</listener>
<listener>
<listener-class>org.jeudego.pairgoth.web.WebappManager</listener-class>
</listener>
<!-- filters -->
<filter>
<filter-name>webapp-slf4j-logger-ip-tag-filter</filter-name>
<filter-class>com.republicate.slf4j.impl.IPTagFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<!-- filters mapping -->
<filter-mapping>
<filter-name>webapp-slf4j-logger-ip-tag-filter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
<!-- servlets -->
<servlet>
<servlet-name>api</servlet-name>
<servlet-class>org.jeudego.pairgoth.web.ApiServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>sse</servlet-name>
<servlet-class>org.jeudego.pairgoth.web.SSEServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<!-- servlet mappings -->
<servlet-mapping>
<servlet-name>api</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>sse</servlet-name>
<url-pattern>/events/*</url-pattern>
</servlet-mapping>
<!-- context params -->
<context-param>
<param-name>webapp-slf4j-logger.format</param-name>
<param-value>%logger [%level] [%ip] %message @%file:%line:%column</param-value>
</context-param>
</web-app>

View File

View File

@@ -0,0 +1,184 @@
package org.jeudego.pairgoth.test
import com.republicate.kson.Json
import org.jeudego.pairgoth.model.ID
import org.junit.jupiter.api.MethodOrderer.MethodName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestMethodOrder
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.fail
@TestMethodOrder(MethodName::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class BasicTests: TestBase() {
val aTournament = Json.Object(
"type" to "INDIVIDUAL",
"name" to "Mon Tournoi",
"shortName" to "mon-tournoi",
"startDate" to "2023-05-10",
"endDate" to "2023-05-12",
"country" to "FR",
"location" to "Marseille",
"online" to false,
"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_SLIP"
)
)
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 "MACMAHON"
)
)
val aPlayer = Json.Object(
"name" to "Burma",
"firstname" to "Nestor",
"rating" to 1600,
"rank" to -5,
"country" to "FR",
"club" to "13Ma"
)
val anotherPlayer = Json.Object(
"name" to "Poirot",
"firstname" to "Hercule",
"rating" to 1700,
"rank" to -1,
"country" to "FR",
"club" to "75Op"
)
var aTournamentID: ID? = null
var aTeamTournamentID: ID? = null
var aPlayerID: ID? = null
var anotherPlayerID: ID? = null
var aTournamentGameID: ID? = null
@Test
fun `001 create tournament`() {
val resp = TestAPI.post("/api/tour", aTournament).asObject()
assertTrue(resp.getBoolean("success") == true, "expecting success")
aTournamentID = resp.getInt("id")
assertNotNull(aTournamentID)
}
@Test
fun `002 get tournament`() {
val resp = TestAPI.get("/api/tour/$aTournamentID").asObject()
assertEquals(aTournamentID, resp.getInt("id"), "First tournament should have id #$aTournamentID")
// filter out "id", and also "komi", "rules" and "gobanSize" which were provided by default
val cmp = Json.Object(*resp.entries.filter { it.key !in listOf("id", "komi", "rules", "gobanSize") }.map { Pair(it.key, it.value) }.toTypedArray())
assertEquals(aTournament.toString(), cmp.toString(), "tournament differs")
}
@Test
fun `003 register user`() {
val resp = TestAPI.post("/api/tour/$aTournamentID/part", aPlayer).asObject()
assertTrue(resp.getBoolean("success") == true, "expecting success")
aPlayerID = resp.getInt("id")
val players = TestAPI.get("/api/tour/$aTournamentID/part").asArray()
val player = players[0] as Json.Object
assertEquals(aPlayerID, player.getInt("id"), "First player should have id #$aPlayerID")
// filter out "id"
val cmp = Json.Object(*player.entries.filter { it.key != "id" }.map { Pair(it.key, it.value) }.toTypedArray())
assertEquals(aPlayer.toString(), cmp.toString(), "player differs")
}
@Test
fun `004 modify user`() {
// remove player aPlayer from round #2
val resp = TestAPI.put("/api/tour/$aTournamentID/part/$aPlayerID", Json.Object("skip" to Json.Array(2))).asObject()
assertTrue(resp.getBoolean("success") == true, "expecting success")
val player = TestAPI.get("/api/tour/$aTournamentID/part/$aPlayerID").asObject()
assertEquals("[2]", player.getArray("skip").toString(), "First player should skip round #2")
}
@Test
fun `005 pair`() {
val resp = TestAPI.post("/api/tour/$aTournamentID/part", anotherPlayer).asObject()
assertTrue(resp.getBoolean("success") == true, "expecting success")
anotherPlayerID = resp.getInt("id")
var games = TestAPI.post("/api/tour/$aTournamentID/pair/1", Json.Array("all")).asArray()
aTournamentGameID = (games[0] as Json.Object).getInt("id")
val possibleResults = setOf(
"""[{"id":$aTournamentGameID,"w":$aPlayerID,"b":$anotherPlayerID,"h":0,"r":"?"}]""",
"""[{"id":$aTournamentGameID,"w":$anotherPlayerID,"b":$aPlayerID,"h":0,"r":"?"}]"""
)
assertTrue(possibleResults.contains(games.toString()), "pairing differs")
games = TestAPI.get("/api/tour/$aTournamentID/res/1").asArray()
assertTrue(possibleResults.contains(games.toString()), "results differs")
val empty = TestAPI.get("/api/tour/$aTournamentID/pair/1").asArray()
assertEquals("[]", empty.toString(), "no more pairables for round 1")
}
@Test
fun `006 result`() {
val resp = TestAPI.put("/api/tour/$aTournamentID/res/1", Json.parse("""{"id":$aTournamentGameID,"result":"b"}""")).asObject()
assertTrue(resp.getBoolean("success") == true, "expecting success")
val games = TestAPI.get("/api/tour/$aTournamentID/res/1")
val possibleResults = setOf(
"""[{"id":$aTournamentGameID,"w":$aPlayerID,"b":$anotherPlayerID,"h":0,"r":"b"}]""",
"""[{"id":$aTournamentGameID,"w":$anotherPlayerID,"b":$aPlayerID,"h":0,"r":"b"}]"""
)
assertTrue(possibleResults.contains(games.toString()), "results differ")
}
@Test
fun `007 team tournament, MacMahon`() {
var resp = TestAPI.post("/api/tour", aTeamTournament).asObject()
assertTrue(resp.getBoolean("success") == true, "expecting success")
aTeamTournamentID = resp.getInt("id")
resp = TestAPI.post("/api/tour/$aTeamTournamentID/part", aPlayer).asObject()
assertTrue(resp.getBoolean("success") == true, "expecting success")
val aTeamPlayerID = resp.getInt("id") ?: fail("id cannot be null")
resp = TestAPI.post("/api/tour/$aTeamTournamentID/part", anotherPlayer).asObject()
assertTrue(resp.getBoolean("success") == true, "expecting success")
val anotherTeamPlayerID = resp.getInt("id") ?: fail("id cannot be null")
var arr = TestAPI.get("/api/tour/$aTeamTournamentID/pair/1").asArray()
assertEquals("[]", arr.toString(), "expecting an empty array")
resp = TestAPI.post("/api/tour/$aTeamTournamentID/team", Json.parse("""{ "name":"The Buffallos", "players":[$aTeamPlayerID, $anotherTeamPlayerID] }""")?.asObject() ?: fail("no null allowed here")).asObject()
assertTrue(resp.getBoolean("success") == true, "expecting success")
val aTeamID = resp.getInt("id") ?: error("no null allowed here")
resp = TestAPI.get("/api/tour/$aTeamTournamentID/team/$aTeamID").asObject()
assertEquals("""{"id":$aTeamID,"name":"The Buffallos","players":[$aTeamPlayerID,$anotherTeamPlayerID]}""", resp.toString(), "expecting team description")
arr = TestAPI.get("/api/tour/$aTeamTournamentID/pair/1").asArray()
assertEquals("[$aTeamID]", arr.toString(), "expecting a singleton array")
// nothing stops us in reusing players in different teams, at least for now...
resp = TestAPI.post("/api/tour/$aTeamTournamentID/team", Json.parse("""{ "name":"The Billies", "players":[$aTeamPlayerID, $anotherTeamPlayerID] }""")?.asObject() ?: fail("no null here")).asObject()
assertTrue(resp.getBoolean("success") == true, "expecting success")
val anotherTeamID = resp.getInt("id") ?: fail("no null here")
arr = TestAPI.get("/api/tour/$aTeamTournamentID/pair/1").asArray()
assertEquals("[$aTeamID,$anotherTeamID]", arr.toString(), "expecting two pairables")
arr = TestAPI.post("/api/tour/$aTeamTournamentID/pair/1", Json.parse("""["all"]""")).asArray()
assertTrue(resp.getBoolean("success") == true, "expecting success")
// TODO check pairing
// val expected = """"["id":1,"w":5,"b":6,"h":3,"r":"?"]"""
}
}

View File

@@ -0,0 +1,27 @@
package org.jeudego.pairgoth.test
import org.junit.jupiter.api.Test
import java.nio.charset.StandardCharsets
class ImportExportTests: TestBase() {
@Test
fun `001 test imports`() {
getTestResources("opengotha").forEach { file ->
logger.info("reading resource ${file.canonicalPath}")
val resource = file.readText(StandardCharsets.UTF_8)
val resp = TestAPI.post("/api/tour", resource)
val id = resp.asObject().getInt("id")
val tournament = TestAPI.get("/api/tour/$id").asObject()
logger.info(tournament.toString().slice(0..50) + "...")
val players = TestAPI.get("/api/tour/$id/part").asArray()
logger.info(players.toString().slice(0..50) + "...")
for (round in 1..tournament.getInt("rounds")!!) {
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.slice(0..50)+"...")
}
}
}

View File

@@ -0,0 +1,24 @@
package org.jeudego.pairgoth.test
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.TestInfo
import org.slf4j.LoggerFactory
import java.io.File
abstract class TestBase {
companion object {
val logger = LoggerFactory.getLogger("test")
@BeforeAll
@JvmStatic
fun prepare() {
}
}
@BeforeEach
fun before(testInfo: TestInfo) {
val testName = testInfo.displayName.removeSuffix("()")
logger.info("===== Running $testName =====")
}
}

View File

@@ -0,0 +1,98 @@
package org.jeudego.pairgoth.test
import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler
import org.jeudego.pairgoth.web.ApiServlet
import org.jeudego.pairgoth.web.SSEServlet
import org.jeudego.pairgoth.web.WebappManager
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import java.io.BufferedReader
import java.io.File
import java.io.IOException
import java.io.PrintWriter
import java.io.StringReader
import java.io.StringWriter
import java.util.*
import java.util.regex.Pattern
import java.util.zip.ZipEntry
import java.util.zip.ZipException
import java.util.zip.ZipFile
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
// J2EE server basic mocking
object TestAPI {
fun Any?.toUnit() = Unit
private val apiServlet = ApiServlet()
private val sseServlet = SSEServlet()
private fun <T> testRequest(reqMethod: String, uri: String, accept: String = "application/json", payload: T? = null): String {
WebappManager.properties["webapp.env"] = "test"
// mock request
val myHeaderNames = if (reqMethod == "GET") emptyList() else listOf("Content-Type")
val selector = argumentCaptor<String>()
val subSelector = argumentCaptor<String>()
val reqPayload = argumentCaptor<String>()
val myReader = payload?.let { BufferedReader(StringReader(payload.toString())) }
val req = mock<HttpServletRequest> {
on { method } doReturn reqMethod
on { requestURI } doReturn uri
on { setAttribute(eq(ApiHandler.SELECTOR_KEY), selector.capture()) } doAnswer {}
on { setAttribute(eq(ApiHandler.SUBSELECTOR_KEY), subSelector.capture()) } doAnswer {}
on { setAttribute(eq(ApiHandler.PAYLOAD_KEY), reqPayload.capture()) } doAnswer {}
on { getAttribute(ApiHandler.SELECTOR_KEY) } doAnswer { selector.allValues.lastOrNull() }
on { getAttribute(ApiHandler.SUBSELECTOR_KEY) } doAnswer { subSelector.allValues.lastOrNull() }
on { getAttribute(ApiHandler.PAYLOAD_KEY) } doAnswer { reqPayload.allValues.lastOrNull() }
on { reader } doReturn myReader
on { scheme } doReturn "http"
on { localName } doReturn "pairgoth"
on { localPort } doReturn 80
on { contextPath } doReturn ""
on { contentType } doReturn if (reqMethod == "GET") null else when (payload) {
is Json -> "application/json; charset=UTF-8"
is String -> "application/xml; charset=UTF-8"
else -> throw Error("unhandled case")
}
on { headerNames } doReturn Collections.enumeration(myHeaderNames)
on { getHeader(eq("Accept")) } doReturn accept
}
// mock response
val buffer = StringWriter()
val errCode = argumentCaptor<Int>()
val errMessage = argumentCaptor<String>()
val resp = mock<HttpServletResponse> {
on { writer } doAnswer { PrintWriter(buffer) }
on { sendError(errCode.capture(), errMessage.capture()) } doAnswer { throw Error("${errCode.lastValue} ${errMessage.lastValue}") }
}
when (reqMethod) {
"GET" -> apiServlet.doGet(req, resp)
"POST" -> apiServlet.doPost(req, resp)
"PUT" -> apiServlet.doPut(req, resp)
"DELETE" -> apiServlet.doDelete(req, resp)
}
return buffer.toString() ?: throw Error("no response 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
fun getTestResources(path: String) = File("${System.getProperty("user.dir")}/src/test/resources/$path").listFiles()

View File

@@ -0,0 +1,194 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Tournament dataVersion="201" gothaMinorVersion="2" gothaVersion="346">
<Players>
<Player agaExpirationDate="" agaId="" club="84Av" country="FR" egfPin="" ffgLicence="0120001" ffgLicenceStatus="L" firstName="Bernard" grade="3K" name="Mignucci" participating="11111111111111111111" rank="3K" rating="1778" ratingOrigin="FFG : -272" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="76Ro" country="FR" egfPin="" ffgLicence="0700255" ffgLicenceStatus="L" firstName="Maleek" grade="2K" name="Lakssil" participating="11111111111111111111" rank="2K" rating="1917" ratingOrigin="FFG : -133" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="92An" country="FR" egfPin="" ffgLicence="0700445" ffgLicenceStatus="-" firstName="Julien" grade="1D" name="Tournellec" participating="11111111111111111111" rank="1D" rating="2050" ratingOrigin="FFG : 0" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="38Gr" country="FR" egfPin="" ffgLicence="1600308" ffgLicenceStatus="L" firstName="Denis" grade="8K" name="Becker" participating="11111111111111111111" rank="8K" rating="1276" ratingOrigin="FFG : -774" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="84Av" country="FR" egfPin="" ffgLicence="1800198" ffgLicenceStatus="L" firstName="Louise" grade="20K" name="Lefebvre" participating="11111111111111111111" rank="20K" rating="-400" ratingOrigin="FFG : -2450" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="84Av" country="FR" egfPin="" ffgLicence="1800199" ffgLicenceStatus="L" firstName="Nina" grade="20K" name="Lefebvre" participating="11111111111111111111" rank="20K" rating="-400" ratingOrigin="FFG : -2450" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="0013006" ffgLicenceStatus="L" firstName="Philippe" grade="1D" name="Guerre-Genton" participating="11100000000000000000" rank="1D" rating="2064" ratingOrigin="FFG : 14" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="15825150" ffgLicence="" ffgLicenceStatus="" firstName="Alexandra" grade="11K" name="Goloubkov" participating="11111111111111111111" rank="11K" rating="1023" ratingOrigin="EGF : 1023" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="8312100" ffgLicenceStatus="L" firstName="Monique" grade="1K" name="Berreby" participating="10000000000000000000" rank="1K" rating="1951" ratingOrigin="FFG : -99" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="34Mo" country="FR" egfPin="" ffgLicence="1700003" ffgLicenceStatus="L" firstName="Morgane" grade="14K" name="Marechal" participating="11111111111111111111" rank="14K" rating="696" ratingOrigin="FFG : -1354" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="9193005" ffgLicenceStatus="L" firstName="Pascal" grade="7K" name="Crepy" participating="11111111111111111111" rank="7K" rating="1373" ratingOrigin="FFG : -677" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="38Gr" country="FR" egfPin="" ffgLicence="0413012" ffgLicenceStatus="L" firstName="Philippe" grade="16K" name="Leclercq" participating="11111111111111111111" rank="16K" rating="523" ratingOrigin="FFG : -1527" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="1800116" ffgLicenceStatus="L" firstName="Jean-François" grade="20K" name="Vaca" participating="11111111111111111111" rank="20K" rating="100" ratingOrigin="FFG : -1950" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="38Gr" country="FR" egfPin="" ffgLicence="0800071" ffgLicenceStatus="L" firstName="Pierre" grade="3K" name="Detivaud" participating="11100000000000000000" rank="3K" rating="1817" ratingOrigin="FFG : -233" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="31To" country="FR" egfPin="" ffgLicence="0491005" ffgLicenceStatus="L" firstName="Minh" grade="5K" name="Nguyen_The" participating="11100000000000000000" rank="5K" rating="1559" ratingOrigin="FFG : -491" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="10374793" ffgLicence="" ffgLicenceStatus="" firstName="Philippe" grade=" 7K" name="Coppey" participating="11111111111111111111" rank="8K" rating="1317" ratingOrigin="EGF : 1317" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="35Re" country="FR" egfPin="" ffgLicence="0800078" ffgLicenceStatus="L" firstName="Rémi" grade="3K" name="Cornaggia" participating="11111111111111111111" rank="3K" rating="1841" ratingOrigin="FFG : -209" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="06Pe" country="Fr" egfPin="" ffgLicence="" ffgLicenceStatus="" firstName="Alexandre" grade="16K" name="Pastorino" participating="11111111111111111111" rank="16K" rating="500" ratingOrigin="INI" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="1700353" ffgLicenceStatus="L" firstName="Yann" grade="20K" name="Buffard" participating="11100000000000000000" rank="20K" rating="-900" ratingOrigin="FFG : -2950" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="38Gr" country="FR" egfPin="" ffgLicence="1600002" ffgLicenceStatus="L" firstName="Till" grade="7K" name="Blanckaert" participating="11111111111111111111" rank="7K" rating="1392" ratingOrigin="FFG : -658" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="44Na" country="FR" egfPin="" ffgLicence="1450001" ffgLicenceStatus="L" firstName="Yvan" grade="5K" name="Martin" participating="11111111111111111111" rank="5K" rating="1567" ratingOrigin="FFG : -483" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="0900460" ffgLicenceStatus="L" firstName="Florent" grade="3D" name="Rioland" participating="11111111111111111111" rank="3D" rating="2336" ratingOrigin="FFG : 286" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="34Mo" country="FR" egfPin="" ffgLicence="1700107" ffgLicenceStatus="L" firstName="Christelle" grade="18K" name="Spano" participating="11111111111111111111" rank="18K" rating="261" ratingOrigin="FFG : -1789" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="06Pe" country="FR" egfPin="" ffgLicence="1500015" ffgLicenceStatus="L" firstName="Stéphane" grade="2K" name="Thao" participating="11111111111111111111" rank="2K" rating="1893" ratingOrigin="FFG : -157" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="38GJ" country="FR" egfPin="" ffgLicence="1300157" ffgLicenceStatus="L" firstName="Aurélien" grade="1D" name="Morel" participating="11111111111111111111" rank="1D" rating="2119" ratingOrigin="FFG : 69" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="1000413" ffgLicenceStatus="L" firstName="Dani" grade="3K" name="Damaz" participating="11111111111111111111" rank="3K" rating="1785" ratingOrigin="FFG : -265" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="84Av" country="FR" egfPin="" ffgLicence="0800122" ffgLicenceStatus="L" firstName="Didier" grade="8K" name="Betored" participating="11111111111111111111" rank="8K" rating="1258" ratingOrigin="FFG : -792" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="38Gr" country="FR" egfPin="" ffgLicence="1200006" ffgLicenceStatus="L" firstName="Déborah" grade="8K" name="Mougin" participating="11111111111111111111" rank="8K" rating="1346" ratingOrigin="FFG : -704" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="83SA" country="FR" egfPin="" ffgLicence="0314300" ffgLicenceStatus="L" firstName="Julie" grade="3K" name="Artigny" participating="11000000000000000000" rank="3K" rating="1790" ratingOrigin="FFG : -260" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="38Gr" country="FR" egfPin="" ffgLicence="1300045" ffgLicenceStatus="L" firstName="Loïc" grade="1D" name="Lefebvre" participating="10111111111111111111" rank="1D" rating="2131" ratingOrigin="FFG : 81" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="0337003" ffgLicenceStatus="-" firstName="Cécile" grade="2K" name="Guibert" participating="11100000000000000000" rank="2K" rating="1850" ratingOrigin="FFG : -200" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="9577000" ffgLicenceStatus="L" firstName="Bernard" grade="2K" name="Coppe" participating="11111111111111111111" rank="2K" rating="1898" ratingOrigin="FFG : -152" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Mr" country="FR" egfPin="" ffgLicence="1400329" ffgLicenceStatus="L" firstName="Axel" grade="1K" name="Bonat" participating="11111111111111111111" rank="1K" rating="1974" ratingOrigin="FFG : -76" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="9224601" ffgLicenceStatus="L" firstName="Julien" grade="4D" name="Roubertie" participating="11111111111111111111" rank="4D" rating="2382" ratingOrigin="FFG : 332" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="84Av" country="FR" egfPin="" ffgLicence="0673005" ffgLicenceStatus="L" firstName="Antoine" grade="2K" name="Lefebvre" participating="11111111111111111111" rank="2K" rating="1941" ratingOrigin="FFG : -109" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="1300334" ffgLicenceStatus="L" firstName="Marie" grade="13K" name="Couffignal" participating="10111111111111111111" rank="13K" rating="784" ratingOrigin="FFG : -1266" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="38Gr" country="FR" egfPin="" ffgLicence="0177002" ffgLicenceStatus="L" firstName="Roland" grade="4K" name="Carbonnel" participating="11011111111111111111" rank="4K" rating="1662" ratingOrigin="FFG : -388" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="06Pe" country="FR" egfPin="" ffgLicence="1800125" ffgLicenceStatus="L" firstName="Cyril" grade="16K" name="Blanco" participating="11111111111111111111" rank="16K" rating="480" ratingOrigin="FFG : -1570" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="84Va" country="FR" egfPin="" ffgLicence="8414001" ffgLicenceStatus="L" firstName="Philippe" grade="4K" name="Brochet" participating="11111111111111111111" rank="4K" rating="1660" ratingOrigin="FFG : -390" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="0677002" ffgLicenceStatus="L" firstName="Caroline" grade="12K" name="Lacroix" participating="11111111111111111111" rank="12K" rating="877" ratingOrigin="FFG : -1173" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="15462271" ffgLicence="" ffgLicenceStatus="" firstName="Cesar" grade=" 3D" name="Lextrait" participating="00000000000000000000" rank="3D" rating="2265" ratingOrigin="EGF : 2265" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="9623561" ffgLicenceStatus="L" firstName="Gaël" grade="7K" name="le_Lidec" participating="11100000000000000000" rank="7K" rating="1418" ratingOrigin="FFG : -632" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="34Mo" country="FR" egfPin="" ffgLicence="1000483" ffgLicenceStatus="L" firstName="Sylvain" grade="5K" name="Col" participating="11111111111111111111" rank="5K" rating="1600" ratingOrigin="FFG : -450" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="38Gr" country="FR" egfPin="" ffgLicence="1500309" ffgLicenceStatus="L" firstName="Robin" grade="2K" name="Stieglitz" participating="10011111111111111111" rank="2K" rating="1916" ratingOrigin="FFG : -134" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="38Gr" country="FR" egfPin="" ffgLicence="1700355" ffgLicenceStatus="L" firstName="Olivier" grade="8K" name="Dumas" participating="11111111111111111111" rank="8K" rating="1271" ratingOrigin="FFG : -779" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="B" country="DE" egfPin="10237205" ffgLicence="" ffgLicenceStatus="" firstName="Adrian" grade=" 4D" name="Passow" participating="11000000000000000000" rank="4D" rating="2354" ratingOrigin="EGF : 2354" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="xxxx" country="PT" egfPin="" ffgLicence="1700354" ffgLicenceStatus="e" firstName="Joao" grade="9K" name="Ferreira" participating="11111111111111111111" rank="9K" rating="1179" ratingOrigin="FFG : -871" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="1000204" ffgLicenceStatus="L" firstName="Philémon" grade="20K" name="le_Lidec" participating="11100000000000000000" rank="20K" rating="-292" ratingOrigin="FFG : -2342" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="06Pe" country="FR" egfPin="" ffgLicence="1700179" ffgLicenceStatus="L" firstName="Kevin" grade="16K" name="Margouillet" participating="11111111111111111111" rank="16K" rating="290" ratingOrigin="FFG : -1760" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="Fr" egfPin="" ffgLicence="" ffgLicenceStatus="" firstName="Romain" grade="15K" name="Pettex" participating="11111111111111111111" rank="15K" rating="600" ratingOrigin="INI" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="34Mo" country="FR" egfPin="18325296" ffgLicence="" ffgLicenceStatus="" firstName="Lisa" grade="16K" name="Imhoff" participating="10011111111111111111" rank="15K" rating="574" ratingOrigin="EGF : 574" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="84Va" country="FR" egfPin="" ffgLicence="0574710" ffgLicenceStatus="L" firstName="Jean-Philippe" grade="2K" name="Hueber" participating="11111111111111111111" rank="2K" rating="1880" ratingOrigin="FFG : -170" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="34Mo" country="FR" egfPin="" ffgLicence="9877402" ffgLicenceStatus="L" firstName="Marie-Françoise" grade="10K" name="Camps" participating="10111111111111111111" rank="10K" rating="1149" ratingOrigin="FFG : -901" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="34Mo" country="FR" egfPin="" ffgLicence="0425022" ffgLicenceStatus="L" firstName="Sylvain" grade="10K" name="Reynal" participating="11100000000000000000" rank="10K" rating="1078" ratingOrigin="FFG : -972" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="69Ly" country="FR" egfPin="" ffgLicence="1300046" ffgLicenceStatus="L" firstName="Guillaume" grade="2K" name="Ghyselen" participating="11000000000000000000" rank="2K" rating="1857" ratingOrigin="FFG : -193" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="0377002" ffgLicenceStatus="L" firstName="Pierre" grade="5K" name="Fronteri" participating="11011111111111111111" rank="5K" rating="1590" ratingOrigin="FFG : -460" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="13513291" ffgLicence="" ffgLicenceStatus="" firstName="Claude" grade=" 6K" name="Brisson" participating="00000000000000000000" rank="6K" rating="1504" ratingOrigin="EGF : 1504" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="38Gr" country="FR" egfPin="" ffgLicence="1500233" ffgLicenceStatus="L" firstName="Macha" grade="7K" name="Jumelin" participating="11111111111111111111" rank="7K" rating="1421" ratingOrigin="FFG : -629" registeringStatus="FIN" smmsCorrection="0"/>
</Players>
<Games>
<Game blackPlayer="ROUBERTIEJULIEN" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="1" whitePlayer="LEFEBVRELOÏC"/>
<Game blackPlayer="VACAJEAN-FRANÇOIS" handicap="2" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="25" whitePlayer="BLANCOCYRIL"/>
<Game blackPlayer="BONATAXEL" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="2" whitePlayer="PASSOWADRIAN"/>
<Game blackPlayer="SPANOCHRISTELLE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="26" whitePlayer="MARGOUILLETKEVIN"/>
<Game blackPlayer="RIOLANDFLORENT" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="3" whitePlayer="GUERRE-GENTONPHILIPPE"/>
<Game blackPlayer="LE_LIDECPHILÉMON" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="27" whitePlayer="LEFEBVRELOUISE"/>
<Game blackPlayer="MORELAURÉLIEN" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="4" whitePlayer="TOURNELLECJULIEN"/>
<Game blackPlayer="BUFFARDYANN" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="28" whitePlayer="LEFEBVRENINA"/>
<Game blackPlayer="THAOSTÉPHANE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="5" whitePlayer="BERREBYMONIQUE"/>
<Game blackPlayer="LEFEBVREANTOINE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="6" whitePlayer="GHYSELENGUILLAUME"/>
<Game blackPlayer="GUIBERTCÉCILE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="7" whitePlayer="LAKSSILMALEEK"/>
<Game blackPlayer="STIEGLITZROBIN" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="8" whitePlayer="HUEBERJEAN-PHILIPPE"/>
<Game blackPlayer="ARTIGNYJULIE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="9" whitePlayer="COPPEBERNARD"/>
<Game blackPlayer="MIGNUCCIBERNARD" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="10" whitePlayer="CORNAGGIARÉMI"/>
<Game blackPlayer="DETIVAUDPIERRE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="11" whitePlayer="DAMAZDANI"/>
<Game blackPlayer="CARBONNELROLAND" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="12" whitePlayer="BROCHETPHILIPPE"/>
<Game blackPlayer="COLSYLVAIN" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="13" whitePlayer="MARTINYVAN"/>
<Game blackPlayer="NGUYEN_THEMINH" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="14" whitePlayer="FRONTERIPIERRE"/>
<Game blackPlayer="CREPYPASCAL" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="15" whitePlayer="JUMELINMACHA"/>
<Game blackPlayer="LE_LIDECGAËL" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="16" whitePlayer="BLANCKAERTTILL"/>
<Game blackPlayer="BETOREDDIDIER" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="17" whitePlayer="MOUGINDÉBORAH"/>
<Game blackPlayer="COPPEYPHILIPPE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="18" whitePlayer="DUMASOLIVIER"/>
<Game blackPlayer="FERREIRAJOAO" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="19" whitePlayer="BECKERDENIS"/>
<Game blackPlayer="LACROIXCAROLINE" handicap="1" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="20" whitePlayer="CAMPSMARIE-FRANÇOISE"/>
<Game blackPlayer="REYNALSYLVAIN" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="21" whitePlayer="GOLOUBKOVALEXANDRA"/>
<Game blackPlayer="MARECHALMORGANE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="22" whitePlayer="COUFFIGNALMARIE"/>
<Game blackPlayer="BLANCOCYRIL" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="23" whitePlayer="PETTEXROMAIN"/>
<Game blackPlayer="PASTORINOALEXANDRE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="24" whitePlayer="IMHOFFLISA"/>
<Game blackPlayer="MARGOUILLETKEVIN" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="25" whitePlayer="LECLERCQPHILIPPE"/>
<Game blackPlayer="LE_LIDECPHILÉMON" handicap="1" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="26" whitePlayer="SPANOCHRISTELLE"/>
<Game blackPlayer="LEFEBVRENINA" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="27" whitePlayer="VACAJEAN-FRANÇOIS"/>
<Game blackPlayer="LEFEBVRELOUISE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="28" whitePlayer="BUFFARDYANN"/>
<Game blackPlayer="COPPEBERNARD" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="1" whitePlayer="MORELAURÉLIEN"/>
<Game blackPlayer="RIOLANDFLORENT" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="2" whitePlayer="ROUBERTIEJULIEN"/>
<Game blackPlayer="LEFEBVREANTOINE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="3" whitePlayer="LEFEBVRELOÏC"/>
<Game blackPlayer="TOURNELLECJULIEN" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="4" whitePlayer="LAKSSILMALEEK"/>
<Game blackPlayer="THAOSTÉPHANE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="5" whitePlayer="STIEGLITZROBIN"/>
<Game blackPlayer="HUEBERJEAN-PHILIPPE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="6" whitePlayer="BONATAXEL"/>
<Game blackPlayer="DAMAZDANI" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="7" whitePlayer="MIGNUCCIBERNARD"/>
<Game blackPlayer="BECKERDENIS" handicap="2" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="8" whitePlayer="BROCHETPHILIPPE"/>
<Game blackPlayer="CORNAGGIARÉMI" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="9" whitePlayer="MARTINYVAN"/>
<Game blackPlayer="CARBONNELROLAND" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="10" whitePlayer="COLSYLVAIN"/>
<Game blackPlayer="BLANCKAERTTILL" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="12" whitePlayer="FRONTERIPIERRE"/>
<Game blackPlayer="COPPEYPHILIPPE" handicap="1" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="13" whitePlayer="JUMELINMACHA"/>
<Game blackPlayer="MOUGINDÉBORAH" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="14" whitePlayer="CREPYPASCAL"/>
<Game blackPlayer="DUMASOLIVIER" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="15" whitePlayer="FERREIRAJOAO"/>
<Game blackPlayer="GOLOUBKOVALEXANDRA" handicap="1" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="17" whitePlayer="BETOREDDIDIER"/>
<Game blackPlayer="COUFFIGNALMARIE" handicap="1" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="18" whitePlayer="CAMPSMARIE-FRANÇOISE"/>
<Game blackPlayer="IMHOFFLISA" handicap="2" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="19" whitePlayer="LACROIXCAROLINE"/>
<Game blackPlayer="PASTORINOALEXANDRE" handicap="2" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="20" whitePlayer="MARECHALMORGANE"/>
<Game blackPlayer="MARGOUILLETKEVIN" handicap="1" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="21" whitePlayer="PETTEXROMAIN"/>
<Game blackPlayer="BLANCOCYRIL" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="22" whitePlayer="LECLERCQPHILIPPE"/>
<Game blackPlayer="VACAJEAN-FRANÇOIS" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="23" whitePlayer="SPANOCHRISTELLE"/>
<Game blackPlayer="LEFEBVRELOUISE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="24" whitePlayer="LEFEBVRENINA"/>
<Game blackPlayer="ROUBERTIEJULIEN" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="1" whitePlayer="MORELAURÉLIEN"/>
<Game blackPlayer="RIOLANDFLORENT" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="2" whitePlayer="TOURNELLECJULIEN"/>
<Game blackPlayer="LEFEBVRELOÏC" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="3" whitePlayer="THAOSTÉPHANE"/>
<Game blackPlayer="GUERRE-GENTONPHILIPPE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="4" whitePlayer="COPPEBERNARD"/>
<Game blackPlayer="LAKSSILMALEEK" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="5" whitePlayer="HUEBERJEAN-PHILIPPE"/>
<Game blackPlayer="MIGNUCCIBERNARD" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="6" whitePlayer="BONATAXEL"/>
<Game blackPlayer="DETIVAUDPIERRE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="7" whitePlayer="LEFEBVREANTOINE"/>
<Game blackPlayer="MARTINYVAN" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="8" whitePlayer="GUIBERTCÉCILE"/>
<Game blackPlayer="CORNAGGIARÉMI" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="9" whitePlayer="DAMAZDANI"/>
<Game blackPlayer="BROCHETPHILIPPE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="10" whitePlayer="NGUYEN_THEMINH"/>
<Game blackPlayer="COLSYLVAIN" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="11" whitePlayer="JUMELINMACHA"/>
<Game blackPlayer="CREPYPASCAL" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="12" whitePlayer="BLANCKAERTTILL"/>
<Game blackPlayer="COPPEYPHILIPPE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="13" whitePlayer="BECKERDENIS"/>
<Game blackPlayer="MOUGINDÉBORAH" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="14" whitePlayer="LE_LIDECGAËL"/>
<Game blackPlayer="BETOREDDIDIER" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="15" whitePlayer="DUMASOLIVIER"/>
<Game blackPlayer="REYNALSYLVAIN" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="16" whitePlayer="FERREIRAJOAO"/>
<Game blackPlayer="GOLOUBKOVALEXANDRA" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="17" whitePlayer="CAMPSMARIE-FRANÇOISE"/>
<Game blackPlayer="MARECHALMORGANE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="18" whitePlayer="LACROIXCAROLINE"/>
<Game blackPlayer="PASTORINOALEXANDRE" handicap="1" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="19" whitePlayer="COUFFIGNALMARIE"/>
<Game blackPlayer="LECLERCQPHILIPPE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="20" whitePlayer="PETTEXROMAIN"/>
<Game blackPlayer="SPANOCHRISTELLE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="21" whitePlayer="BLANCOCYRIL"/>
<Game blackPlayer="BUFFARDYANN" handicap="3" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="22" whitePlayer="MARGOUILLETKEVIN"/>
<Game blackPlayer="VACAJEAN-FRANÇOIS" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="23" whitePlayer="LEFEBVRELOUISE"/>
<Game blackPlayer="LEFEBVRENINA" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="24" whitePlayer="LE_LIDECPHILÉMON"/>
<Game blackPlayer="PASSOWADRIAN" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="1" whitePlayer="ROUBERTIEJULIEN"/>
<Game blackPlayer="MORELAURÉLIEN" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="2" whitePlayer="RIOLANDFLORENT"/>
<Game blackPlayer="TOURNELLECJULIEN" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="4" whitePlayer="GUERRE-GENTONPHILIPPE"/>
<Game blackPlayer="LAKSSILMALEEK" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="5" whitePlayer="BONATAXEL"/>
<Game blackPlayer="COPPEBERNARD" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="7" whitePlayer="LEFEBVREANTOINE"/>
<Game blackPlayer="THAOSTÉPHANE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="8" whitePlayer="GUIBERTCÉCILE"/>
<Game blackPlayer="HUEBERJEAN-PHILIPPE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="9" whitePlayer="MIGNUCCIBERNARD"/>
<Game blackPlayer="GHYSELENGUILLAUME" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="10" whitePlayer="DETIVAUDPIERRE"/>
<Game blackPlayer="BROCHETPHILIPPE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="11" whitePlayer="CORNAGGIARÉMI"/>
<Game blackPlayer="DAMAZDANI" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="12" whitePlayer="ARTIGNYJULIE"/>
<Game blackPlayer="NGUYEN_THEMINH" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="13" whitePlayer="CARBONNELROLAND"/>
<Game blackPlayer="FRONTERIPIERRE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="14" whitePlayer="MARTINYVAN"/>
<Game blackPlayer="BLANCKAERTTILL" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="15" whitePlayer="COLSYLVAIN"/>
<Game blackPlayer="JUMELINMACHA" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="16" whitePlayer="BETOREDDIDIER"/>
<Game blackPlayer="BECKERDENIS" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="17" whitePlayer="LE_LIDECGAËL"/>
<Game blackPlayer="DUMASOLIVIER" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="18" whitePlayer="CREPYPASCAL"/>
<Game blackPlayer="MOUGINDÉBORAH" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="19" whitePlayer="COPPEYPHILIPPE"/>
<Game blackPlayer="GOLOUBKOVALEXANDRA" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="20" whitePlayer="FERREIRAJOAO"/>
<Game blackPlayer="LACROIXCAROLINE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="22" whitePlayer="REYNALSYLVAIN"/>
<Game blackPlayer="PETTEXROMAIN" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="23" whitePlayer="MARECHALMORGANE"/>
<Game blackPlayer="LECLERCQPHILIPPE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="24" whitePlayer="PASTORINOALEXANDRE"/>
</Games>
<TournamentParameterSet>
<GeneralParameterSet basicTime="60" beginDate="2018-09-07" canByoYomiTime="300" complementaryTimeSystem="CANBYOYOMI" director="Brisson" endDate="2018-09-09" fischerTime="10" genCountNotPlayedGamesAsHalfPoint="false" genMMBar="1D" genMMFloor="20K" genMMS2ValueAbsent="1" genMMS2ValueBye="2" genMMZero="30K" genNBW2ValueAbsent="0" genNBW2ValueBye="2" genRoundDownNBWMMS="true" komi="7.5" location="Frioul" name="Frioul 2018" nbMovesCanTime="15" numberOfCategories="1" numberOfRounds="4" shortName="Frioul" size="19" stdByoYomiTime="30"/>
<HandicapParameterSet hdBasedOnMMS="true" hdCeiling="9" hdCorrection="1" hdNoHdRankThreshold="1D"/>
<PlacementParameterSet>
<PlacementCriteria>
<PlacementCriterion name="MMS" number="1"/>
<PlacementCriterion name="SOSM" number="2"/>
<PlacementCriterion name="SOSOSM" 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="NULL" paiMaAvoidMixingCategories="0" paiMaCompensateDUDD="true" paiMaDUDDLowerMode="MID" paiMaDUDDUpperMode="MID" paiMaDUDDWeight="100000000" paiMaLastRoundForSeedSystem1="2" paiMaMaximizeSeeding="5000000" paiMaMinimizeScoreDifference="100000000000" paiMaSeedSystem1="SPLITANDRANDOM" paiSeAvoidSameGeo="100000000000" paiSeBarThresholdActive="true" paiSeDefSecCrit="100000000000" paiSeMinimizeHandicap="0" paiSeNbWinsThresholdActive="false" paiSePreferMMSDiffRatherThanSameClub="3" paiSePreferMMSDiffRatherThanSameClubsGroup="2" paiSePreferMMSDiffRatherThanSameCountry="1" paiSeRankThreshold="1D" paiStandardNX1Factor="0.5"/>
<DPParameterSet displayClCol="true" displayCoCol="true" displayIndGamesInMatches="true" displayNPPlayers="false" displayNumCol="true" displayPlCol="true" gameFormat="full" playerSortType="name" showByePlayer="true" showNotFinallyRegisteredPlayers="true" showNotPairedPlayers="true" showNotParticipatingPlayers="false" showPlayerClub="true" showPlayerCountry="false" showPlayerGrade="true"/>
<PublishParameterSet exportToLocalFile="true" htmlAutoScroll="false" print="true"/>
</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>

View File

@@ -0,0 +1,184 @@
<?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>
<Player agaExpirationDate="" agaId="" club="84Av" country="FR" egfPin="" ffgLicence="0120001" ffgLicenceStatus="L" firstName="Bernard" grade="3K" name="Mignucci" participating="11111111111111111111" rank="3K" rating="1820" ratingOrigin="FFG : -230" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="75Pa" country="FR" egfPin="" ffgLicence="0220802" ffgLicenceStatus="L" firstName="David" grade="2K" name="Nicolas" participating="11111111111111111111" rank="2K" rating="1893" ratingOrigin="FFG : -157" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="69Ly" country="FR" egfPin="" ffgLicence="8414002" ffgLicenceStatus="L" firstName="Jean-Christophe" grade="3K" name="Honoré" participating="11111111111111111111" rank="3K" rating="1796" ratingOrigin="FFG : -254" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="0013006" ffgLicenceStatus="L" firstName="Philippe" grade="1K" name="Guerre-Genton" participating="11111111111111111111" rank="1K" rating="2042" ratingOrigin="FFG : -8" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="91Or" country="FR" egfPin="" ffgLicence="9826801" ffgLicenceStatus="L" firstName="Ralf" grade="2K" name="Wurzinger" participating="11111111111111111111" rank="2K" rating="1893" ratingOrigin="FFG : -157" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="47Ag" country="FR" egfPin="" ffgLicence="0652005" ffgLicenceStatus="L" firstName="Jean-Pierre" grade="11K" name="Ladet" participating="11111111111111111111" rank="11K" rating="1042" ratingOrigin="FFG : -1008" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="8312100" ffgLicenceStatus="L" firstName="Monique" grade="2K" name="Berreby" participating="11111111111111111111" rank="2K" rating="1915" ratingOrigin="FFG : -135" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="06Pe" country="FR" egfPin="" ffgLicence="0370000" ffgLicenceStatus="L" firstName="Rémi" grade="8K" name="Butaud" participating="11111111111111111111" rank="8K" rating="1325" ratingOrigin="FFG : -725" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="63Ce" country="FR" egfPin="" ffgLicence="1500110" ffgLicenceStatus="L" firstName="Jean-Louis" grade="10K" name="Trinquand" participating="11111111111111111111" rank="10K" rating="1145" ratingOrigin="FFG : -905" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="30Al" country="FR" egfPin="" ffgLicence="9177404" ffgLicenceStatus="L" firstName="Denis" grade="1D" name="Feldmann" participating="11111111111111111111" rank="1D" rating="2076" ratingOrigin="FFG : 26" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="38Gr" country="FR" egfPin="" ffgLicence="7904900" ffgLicenceStatus="L" firstName="Dominique" grade="1D" name="Cornuejols" participating="11111111111111111111" rank="1D" rating="2105" ratingOrigin="FFG : 55" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ai" country="FR" egfPin="" ffgLicence="9125031" ffgLicenceStatus="L" firstName="Stéphane" grade="1K" name="Poisson" participating="11111111111111111111" rank="1K" rating="1954" ratingOrigin="FFG : -96" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="30LV" country="FR" egfPin="" ffgLicence="1700164" ffgLicenceStatus="L" firstName="Stephan" grade="10K" name="Habuda" participating="11111111111111111111" rank="10K" rating="1109" ratingOrigin="FFG : -941" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="30LV" country="FR" egfPin="" ffgLicence="1900261" ffgLicenceStatus="L" firstName="Bruno" grade="11K" name="Martin-Vallas" participating="11111111111111111111" rank="11K" rating="959" ratingOrigin="FFG : -1091" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="76Ro" country="FR" egfPin="" ffgLicence="8665300" ffgLicenceStatus="L" firstName="Jean-Luc" grade="9K" name="Gaillard" participating="11111111111111111111" rank="9K" rating="1154" ratingOrigin="FFG : -896" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="75Pa" country="FR" egfPin="" ffgLicence="1800039" ffgLicenceStatus="L" firstName="Gilles" grade="7K" name="Habart" participating="11111111111111111111" rank="7K" rating="1358" ratingOrigin="FFG : -692" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="75Op" country="FR" egfPin="" ffgLicence="7907800" ffgLicenceStatus="L" firstName="Jérôme" grade="3D" name="Hubert" participating="11111111111111111111" rank="3D" rating="2326" ratingOrigin="FFG : 276" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="75Op" country="FR" egfPin="" ffgLicence="9425017" ffgLicenceStatus="L" firstName="Simon" grade="1K" name="Rosenblatt" participating="11000000000000000000" rank="1K" rating="1978" ratingOrigin="FFG : -72" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="64Ba" country="FR" egfPin="" ffgLicence="1200009" ffgLicenceStatus="C" firstName="Philippe" grade="3K" name="Batailler" participating="00000000000000000000" rank="3K" rating="1822" ratingOrigin="FFG : -228" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="35Re" country="FR" egfPin="" ffgLicence="9737213" ffgLicenceStatus="L" firstName="Alain" grade="5K" name="Papazoglou" participating="11111111111111111111" rank="5K" rating="1571" ratingOrigin="FFG : -479" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="86Po" country="FR" egfPin="" ffgLicence="9025075" ffgLicenceStatus="L" firstName="Jean-François" grade="6K" name="Thovert" participating="11111111111111111111" rank="6K" rating="1546" ratingOrigin="FFG : -504" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="31Ba" country="FR" egfPin="" ffgLicence="0425000" ffgLicenceStatus="L" firstName="Philippe" grade="7K" name="Grimond" participating="11111111111111111111" rank="7K" rating="1442" ratingOrigin="FFG : -608" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="86Po" country="FR" egfPin="" ffgLicence="9725084" ffgLicenceStatus="L" firstName="Fabrice" grade="7K" name="Neant" participating="11111111111111111111" rank="7K" rating="1396" ratingOrigin="FFG : -654" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="31Ba" country="FR" egfPin="" ffgLicence="0700101" ffgLicenceStatus="L" firstName="Jean-Yves" grade="10K" name="Papazoglou" participating="11111111111111111111" rank="10K" rating="1104" ratingOrigin="FFG : -946" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="75Al" country="FR" egfPin="" ffgLicence="1400130" ffgLicenceStatus="L" firstName="Guy" grade="20K" name="Jollivet" participating="11111111111111111111" rank="20K" rating="53" ratingOrigin="FFG : -1997" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="63Ce" country="FR" egfPin="" ffgLicence="2100032" ffgLicenceStatus="L" firstName="William" grade="30K" name="Dupré" participating="11111111111111111111" rank="30K" rating="-900" ratingOrigin="FFG : -9999" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="31Ba" country="FR" egfPin="" ffgLicence="2100041" ffgLicenceStatus="L" firstName="Margherita" grade="15K" name="Orsino" participating="11111111111111111111" rank="15K" rating="558" ratingOrigin="FFG : -1492" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="35Re" country="FR" egfPin="" ffgLicence="9237201" ffgLicenceStatus="L" firstName="Marc" grade="4K" name="Guillou" participating="10000000000000000000" rank="4K" rating="1726" ratingOrigin="FFG : -324" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="44Na" country="FR" egfPin="" ffgLicence="8004400" ffgLicenceStatus="L" firstName="Marc" grade="11K" name="Jegou" participating="11111111111111111111" rank="11K" rating="972" ratingOrigin="FFG : -1078" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="66Pe" country="FR" egfPin="" ffgLicence="9378001" ffgLicenceStatus="L" firstName="Daniel" grade="3K" name="Tosetto" participating="11111111111111111111" rank="3K" rating="1836" ratingOrigin="FFG : -214" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="44Na" country="FR" egfPin="" ffgLicence="1450001" ffgLicenceStatus="L" firstName="Yvan" grade="4K" name="Martin" participating="11111111111111111111" rank="4K" rating="1661" ratingOrigin="FFG : -389" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="34Mo" country="FR" egfPin="" ffgLicence="2000244" ffgLicenceStatus="L" firstName="Véronique" grade="30K" name="Born" participating="11111111111111111111" rank="30K" rating="-900" ratingOrigin="FFG : -9999" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="64Pa" country="FR" egfPin="" ffgLicence="9251702" ffgLicenceStatus="C" firstName="Michel" grade="7K" name="Bonis" participating="11111111111111111111" rank="7K" rating="1400" ratingOrigin="FFG : -650" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="75Al" country="FR" egfPin="" ffgLicence="8696800" ffgLicenceStatus="L" firstName="Christian" grade="6K" name="Boyart" participating="11111111111111111111" rank="6K" rating="1511" ratingOrigin="FFG : -539" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="31Ba" country="FR" egfPin="" ffgLicence="9721004" ffgLicenceStatus="L" firstName="Laurent" grade="3K" name="Lamôle" participating="11111111111111111111" rank="3K" rating="1794" ratingOrigin="FFG : -256" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="64Pa" country="FR" egfPin="" ffgLicence="2100010" ffgLicenceStatus="L" firstName="Serge" grade="5K" name="Eon" participating="11111111111111111111" rank="5K" rating="1592" ratingOrigin="FFG : -458" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="63Ce" country="FR" egfPin="" ffgLicence="9838001" ffgLicenceStatus="L" firstName="Chantal" grade="5K" name="Gajdos" participating="11111111111111111111" rank="5K" rating="1562" ratingOrigin="FFG : -488" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="91Or" country="FR" egfPin="" ffgLicence="0321007" ffgLicenceStatus="C" firstName="Paul" grade="2K" name="Baratou" participating="11111111111111111111" rank="2K" rating="1892" ratingOrigin="FFG : -158" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="44Na" country="FR" egfPin="" ffgLicence="7911800" ffgLicenceStatus="L" firstName="François" grade="2D" name="Mizessyn" participating="11111111111111111111" rank="2D" rating="2228" ratingOrigin="FFG : 178" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="44Na" country="FR" egfPin="" ffgLicence="7920001" ffgLicenceStatus="L" firstName="Frédéric" grade="5D" name="Donzet" participating="11111111111111111111" rank="5D" rating="2485" ratingOrigin="FFG : 435" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="91SM" country="FR" egfPin="" ffgLicence="1400089" ffgLicenceStatus="L" firstName="Nicita" grade="10K" name="Giovanni" participating="11111111111111111111" rank="10K" rating="1065" ratingOrigin="FFG : -985" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="13Ma" country="FR" egfPin="" ffgLicence="0322103" ffgLicenceStatus="L" firstName="Claude" grade="7K" name="Brisson" participating="11111111111111111111" rank="7K" rating="1387" ratingOrigin="FFG : -663" registeringStatus="FIN" smmsCorrection="0"/>
<Player agaExpirationDate="" agaId="" club="31Ba" country="FR" egfPin="" ffgLicence="8210000" ffgLicenceStatus="L" firstName="Denis" grade="7K" name="Puaud" participating="11111111111111111111" rank="7K" rating="1352" ratingOrigin="FFG : -698" registeringStatus="FIN" smmsCorrection="0"/>
</Players>
<Games>
<Game blackPlayer="DONZETFRÉDÉRIC" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="1" whitePlayer="ROSENBLATTSIMON"/>
<Game blackPlayer="POISSONSTÉPHANE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="2" whitePlayer="HUBERTJÉRÔME"/>
<Game blackPlayer="MIZESSYNFRANÇOIS" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="3" whitePlayer="BERREBYMONIQUE"/>
<Game blackPlayer="WURZINGERRALF" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="4" whitePlayer="FELDMANNDENIS"/>
<Game blackPlayer="CORNUEJOLSDOMINIQUE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="5" whitePlayer="NICOLASDAVID"/>
<Game blackPlayer="BARATOUPAUL" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="6" whitePlayer="GUERRE-GENTONPHILIPPE"/>
<Game blackPlayer="TOSETTODANIEL" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="7" whitePlayer="EONSERGE"/>
<Game blackPlayer="PAPAZOGLOUALAIN" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="8" whitePlayer="MIGNUCCIBERNARD"/>
<Game blackPlayer="HONORÉJEAN-CHRISTOPHE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="9" whitePlayer="GAJDOSCHANTAL"/>
<Game blackPlayer="THOVERTJEAN-FRANÇOIS" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="10" whitePlayer="LAMÔLELAURENT"/>
<Game blackPlayer="GUILLOUMARC" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="11" whitePlayer="BOYARTCHRISTIAN"/>
<Game blackPlayer="GRIMONDPHILIPPE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="12" whitePlayer="MARTINYVAN"/>
<Game blackPlayer="BONISMICHEL" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="13" whitePlayer="GAILLARDJEAN-LUC"/>
<Game blackPlayer="HABARTGILLES" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="14" whitePlayer="TRINQUANDJEAN-LOUIS"/>
<Game blackPlayer="BRISSONCLAUDE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="15" whitePlayer="HABUDASTEPHAN"/>
<Game blackPlayer="PAPAZOGLOUJEAN-YVES" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="16" whitePlayer="PUAUDDENIS"/>
<Game blackPlayer="NEANTFABRICE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="17" whitePlayer="GIOVANNINICITA"/>
<Game blackPlayer="BUTAUDRÉMI" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="18" whitePlayer="LADETJEAN-PIERRE"/>
<Game blackPlayer="JEGOUMARC" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="19" whitePlayer="JOLLIVETGUY"/>
<Game blackPlayer="MARTIN-VALLASBRUNO" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="1" tableNumber="20" whitePlayer="BORNVÉRONIQUE"/>
<Game blackPlayer="ORSINOMARGHERITA" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="1" tableNumber="21" whitePlayer="DUPRÉWILLIAM"/>
<Game blackPlayer="HUBERTJÉRÔME" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="5" tableNumber="1" whitePlayer="DONZETFRÉDÉRIC"/>
<Game blackPlayer="MIZESSYNFRANÇOIS" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="5" tableNumber="2" whitePlayer="NEANTFABRICE"/>
<Game blackPlayer="FELDMANNDENIS" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="5" tableNumber="3" whitePlayer="CORNUEJOLSDOMINIQUE"/>
<Game blackPlayer="MIGNUCCIBERNARD" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="5" tableNumber="4" whitePlayer="NICOLASDAVID"/>
<Game blackPlayer="LAMÔLELAURENT" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="5" tableNumber="5" whitePlayer="BUTAUDRÉMI"/>
<Game blackPlayer="GAILLARDJEAN-LUC" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="5" tableNumber="6" whitePlayer="GUERRE-GENTONPHILIPPE"/>
<Game blackPlayer="POISSONSTÉPHANE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="5" tableNumber="7" whitePlayer="BERREBYMONIQUE"/>
<Game blackPlayer="TOSETTODANIEL" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="5" tableNumber="8" whitePlayer="BARATOUPAUL"/>
<Game blackPlayer="HONORÉJEAN-CHRISTOPHE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="5" tableNumber="9" whitePlayer="MARTINYVAN"/>
<Game blackPlayer="THOVERTJEAN-FRANÇOIS" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="5" tableNumber="10" whitePlayer="GAJDOSCHANTAL"/>
<Game blackPlayer="BOYARTCHRISTIAN" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="5" tableNumber="11" whitePlayer="GRIMONDPHILIPPE"/>
<Game blackPlayer="BRISSONCLAUDE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="5" tableNumber="12" whitePlayer="BONISMICHEL"/>
<Game blackPlayer="TRINQUANDJEAN-LOUIS" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="5" tableNumber="13" whitePlayer="PUAUDDENIS"/>
<Game blackPlayer="GIOVANNINICITA" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="5" tableNumber="14" whitePlayer="PAPAZOGLOUJEAN-YVES"/>
<Game blackPlayer="DUPRÉWILLIAM" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="5" tableNumber="15" whitePlayer="LADETJEAN-PIERRE"/>
<Game blackPlayer="EONSERGE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="5" tableNumber="16" whitePlayer="WURZINGERRALF"/>
<Game blackPlayer="HABARTGILLES" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="5" tableNumber="17" whitePlayer="PAPAZOGLOUALAIN"/>
<Game blackPlayer="JEGOUMARC" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="5" tableNumber="18" whitePlayer="HABUDASTEPHAN"/>
<Game blackPlayer="MARTIN-VALLASBRUNO" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="5" tableNumber="19" whitePlayer="ORSINOMARGHERITA"/>
<Game blackPlayer="BORNVÉRONIQUE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="5" tableNumber="20" whitePlayer="JOLLIVETGUY"/>
<Game blackPlayer="DONZETFRÉDÉRIC" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="1" whitePlayer="MIZESSYNFRANÇOIS"/>
<Game blackPlayer="HUBERTJÉRÔME" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="2" whitePlayer="FELDMANNDENIS"/>
<Game blackPlayer="NEANTFABRICE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="3" whitePlayer="BERREBYMONIQUE"/>
<Game blackPlayer="BARATOUPAUL" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="4" whitePlayer="CORNUEJOLSDOMINIQUE"/>
<Game blackPlayer="NICOLASDAVID" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="5" whitePlayer="TOSETTODANIEL"/>
<Game blackPlayer="MIGNUCCIBERNARD" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="6" whitePlayer="BOYARTCHRISTIAN"/>
<Game blackPlayer="GRIMONDPHILIPPE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="7" whitePlayer="LAMÔLELAURENT"/>
<Game blackPlayer="BUTAUDRÉMI" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="8" whitePlayer="BRISSONCLAUDE"/>
<Game blackPlayer="PUAUDDENIS" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="9" whitePlayer="GAILLARDJEAN-LUC"/>
<Game blackPlayer="TRINQUANDJEAN-LOUIS" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="10" whitePlayer="GUERRE-GENTONPHILIPPE"/>
<Game blackPlayer="PAPAZOGLOUJEAN-YVES" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="11" whitePlayer="POISSONSTÉPHANE"/>
<Game blackPlayer="MARTINYVAN" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="12" whitePlayer="WURZINGERRALF"/>
<Game blackPlayer="PAPAZOGLOUALAIN" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="13" whitePlayer="HONORÉJEAN-CHRISTOPHE"/>
<Game blackPlayer="GAJDOSCHANTAL" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="14" whitePlayer="EONSERGE"/>
<Game blackPlayer="HABARTGILLES" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="15" whitePlayer="THOVERTJEAN-FRANÇOIS"/>
<Game blackPlayer="BONISMICHEL" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="16" whitePlayer="HABUDASTEPHAN"/>
<Game blackPlayer="GIOVANNINICITA" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="17" whitePlayer="JEGOUMARC"/>
<Game blackPlayer="LADETJEAN-PIERRE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="4" tableNumber="18" whitePlayer="MARTIN-VALLASBRUNO"/>
<Game blackPlayer="JOLLIVETGUY" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="19" whitePlayer="DUPRÉWILLIAM"/>
<Game blackPlayer="BORNVÉRONIQUE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="4" tableNumber="20" whitePlayer="ORSINOMARGHERITA"/>
<Game blackPlayer="LAMÔLELAURENT" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="1" whitePlayer="DONZETFRÉDÉRIC"/>
<Game blackPlayer="BOYARTCHRISTIAN" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="2" whitePlayer="HUBERTJÉRÔME"/>
<Game blackPlayer="MIZESSYNFRANÇOIS" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="3" whitePlayer="GRIMONDPHILIPPE"/>
<Game blackPlayer="BRISSONCLAUDE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="4" whitePlayer="FELDMANNDENIS"/>
<Game blackPlayer="POISSONSTÉPHANE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="5" whitePlayer="NEANTFABRICE"/>
<Game blackPlayer="BERREBYMONIQUE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="6" whitePlayer="PAPAZOGLOUJEAN-YVES"/>
<Game blackPlayer="GUERRE-GENTONPHILIPPE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="7" whitePlayer="CORNUEJOLSDOMINIQUE"/>
<Game blackPlayer="HONORÉJEAN-CHRISTOPHE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="8" whitePlayer="NICOLASDAVID"/>
<Game blackPlayer="EONSERGE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="9" whitePlayer="BARATOUPAUL"/>
<Game blackPlayer="TOSETTODANIEL" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="10" whitePlayer="GAJDOSCHANTAL"/>
<Game blackPlayer="THOVERTJEAN-FRANÇOIS" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="11" whitePlayer="MIGNUCCIBERNARD"/>
<Game blackPlayer="BONISMICHEL" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="12" whitePlayer="BUTAUDRÉMI"/>
<Game blackPlayer="HABARTGILLES" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="13" whitePlayer="GAILLARDJEAN-LUC"/>
<Game blackPlayer="HABUDASTEPHAN" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="14" whitePlayer="PUAUDDENIS"/>
<Game blackPlayer="DUPRÉWILLIAM" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="15" whitePlayer="TRINQUANDJEAN-LOUIS"/>
<Game blackPlayer="MARTINYVAN" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="16" whitePlayer="JEGOUMARC"/>
<Game blackPlayer="WURZINGERRALF" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="3" tableNumber="17" whitePlayer="MARTIN-VALLASBRUNO"/>
<Game blackPlayer="JOLLIVETGUY" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="18" whitePlayer="PAPAZOGLOUALAIN"/>
<Game blackPlayer="BORNVÉRONIQUE" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="19" whitePlayer="GIOVANNINICITA"/>
<Game blackPlayer="ORSINOMARGHERITA" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="3" tableNumber="20" whitePlayer="LADETJEAN-PIERRE"/>
<Game blackPlayer="BARATOUPAUL" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="1" whitePlayer="DONZETFRÉDÉRIC"/>
<Game blackPlayer="HUBERTJÉRÔME" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="2" whitePlayer="TOSETTODANIEL"/>
<Game blackPlayer="MIGNUCCIBERNARD" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="3" whitePlayer="MIZESSYNFRANÇOIS"/>
<Game blackPlayer="FELDMANNDENIS" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="4" whitePlayer="HONORÉJEAN-CHRISTOPHE"/>
<Game blackPlayer="NICOLASDAVID" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="5" whitePlayer="LAMÔLELAURENT"/>
<Game blackPlayer="BOYARTCHRISTIAN" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="6" whitePlayer="BUTAUDRÉMI"/>
<Game blackPlayer="TRINQUANDJEAN-LOUIS" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="7" whitePlayer="GRIMONDPHILIPPE"/>
<Game blackPlayer="PAPAZOGLOUJEAN-YVES" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="8" whitePlayer="BONISMICHEL"/>
<Game blackPlayer="JEGOUMARC" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="9" whitePlayer="BRISSONCLAUDE"/>
<Game blackPlayer="MARTIN-VALLASBRUNO" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="10" whitePlayer="NEANTFABRICE"/>
<Game blackPlayer="BERREBYMONIQUE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="11" whitePlayer="DUPRÉWILLIAM"/>
<Game blackPlayer="CORNUEJOLSDOMINIQUE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="12" whitePlayer="WURZINGERRALF"/>
<Game blackPlayer="GUERRE-GENTONPHILIPPE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="13" whitePlayer="MARTINYVAN"/>
<Game blackPlayer="ROSENBLATTSIMON" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="14" whitePlayer="EONSERGE"/>
<Game blackPlayer="PAPAZOGLOUALAIN" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="15" whitePlayer="POISSONSTÉPHANE"/>
<Game blackPlayer="GIOVANNINICITA" handicap="0" knownColor="true" result="RESULT_WHITEWINS" roundNumber="2" tableNumber="16" whitePlayer="THOVERTJEAN-FRANÇOIS"/>
<Game blackPlayer="LADETJEAN-PIERRE" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="17" whitePlayer="HABARTGILLES"/>
<Game blackPlayer="PUAUDDENIS" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="18" whitePlayer="ORSINOMARGHERITA"/>
<Game blackPlayer="GAILLARDJEAN-LUC" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="19" whitePlayer="JOLLIVETGUY"/>
<Game blackPlayer="HABUDASTEPHAN" handicap="0" knownColor="true" result="RESULT_BLACKWINS" roundNumber="2" tableNumber="20" whitePlayer="BORNVÉRONIQUE"/>
</Games>
<ByePlayers>
<ByePlayer player="GAJDOSCHANTAL" roundNumber="2"/>
</ByePlayers>
<TournamentParameterSet>
<GeneralParameterSet bInternet="false" basicTime="60" beginDate="2020-12-22" canByoYomiTime="300" complementaryTimeSystem="STDBYOYOMI" director="François Mizessyn" endDate="2021-01-07" fischerTime="10" genCountNotPlayedGamesAsHalfPoint="false" genMMBar="9D" genMMFloor="30K" genMMS2ValueAbsent="1" genMMS2ValueBye="2" genMMZero="30K" genNBW2ValueAbsent="0" genNBW2ValueBye="2" genRoundDownNBWMMS="true" komi="7.5" location="Internet" name="Championnat des Vieux Dragons" nbMovesCanTime="15" numberOfCategories="1" numberOfRounds="5" shortName="vieuxdragons" size="19" stdByoYomiTime="30"/>
<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>