Reenginering to prepare view webapp
This commit is contained in:
295
api-webapp/pom.xml
Normal file
295
api-webapp/pom.xml
Normal file
@@ -0,0 +1,295 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.jeudego.pairgoth</groupId>
|
||||
<artifactId>engine-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>api-webapp</artifactId>
|
||||
|
||||
<packaging>war</packaging>
|
||||
<name>${project.groupId}:${project.artifactId}</name>
|
||||
<description>PairGoth pairing system</description>
|
||||
<url>TODO</url>
|
||||
<properties>
|
||||
<kotlin.version>1.8.21</kotlin.version>
|
||||
<kotlin.code.style>official</kotlin.code.style>
|
||||
<kotlin.compiler.jvmTarget>10</kotlin.compiler.jvmTarget>
|
||||
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
|
||||
<pac4j.version>5.7.1</pac4j.version>
|
||||
</properties>
|
||||
<build>
|
||||
<defaultGoal>package</defaultGoal>
|
||||
<sourceDirectory>src/main/kotlin</sourceDirectory>
|
||||
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>enforce-properties-file-exists</id>
|
||||
<goals>
|
||||
<goal>enforce</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<requireFilesExist>
|
||||
<message>Missing pairgoth.properties file</message>
|
||||
<files>
|
||||
<file>${project.parent.basedir}/pairgoth.properties</file>
|
||||
</files>
|
||||
</requireFilesExist>
|
||||
</rules>
|
||||
<fail>true</fail>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-resources</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>${basedir}/..</directory>
|
||||
<include>pairgoth.properties</include>
|
||||
<!-- <filtering>true</filtering> -->
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<classpathDependencyExcludes>
|
||||
<classpathDependencyExclude>com.republicate:webapp-slf4j-logger</classpathDependencyExclude>
|
||||
</classpathDependencyExcludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit</groupId>
|
||||
<artifactId>junit-bom</artifactId>
|
||||
<version>5.9.3</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- main dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-test-junit5</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-reflect</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-datetime-jvm</artifactId>
|
||||
<version>0.4.0</version>
|
||||
</dependency>
|
||||
<!-- servlets and mail APIs -->
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<version>${servlet.api.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
<artifactId>jakarta.mail</artifactId>
|
||||
<version>1.6.7</version>
|
||||
</dependency>
|
||||
<!-- auth -->
|
||||
<dependency>
|
||||
<groupId>org.pac4j</groupId>
|
||||
<artifactId>pac4j-oauth</artifactId>
|
||||
<version>${pac4j.version}</version>
|
||||
</dependency>
|
||||
<!-- logging -->
|
||||
<dependency>
|
||||
<groupId>io.github.microutils</groupId>
|
||||
<artifactId>kotlin-logging-jvm</artifactId>
|
||||
<version>3.0.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.republicate</groupId>
|
||||
<artifactId>webapp-slf4j-logger</artifactId>
|
||||
<version>3.0</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!--
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>jcl-over-slf4j</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>com.diogonunes</groupId>
|
||||
<artifactId>JColor</artifactId>
|
||||
<version>5.0.1</version>
|
||||
</dependency>
|
||||
<!-- mailer -->
|
||||
<dependency>
|
||||
<groupId>com.republicate</groupId>
|
||||
<artifactId>simple-mailer</artifactId>
|
||||
<version>1.6</version>
|
||||
</dependency>
|
||||
<!-- json -->
|
||||
<dependency>
|
||||
<groupId>com.republicate.kson</groupId>
|
||||
<artifactId>essential-kson-jvm</artifactId>
|
||||
<version>2.3</version>
|
||||
</dependency>
|
||||
<!-- charset detection
|
||||
<dependency>
|
||||
<groupId>com.ibm.icu</groupId>
|
||||
<artifactId>icu4j</artifactId>
|
||||
<version>70.1</version>
|
||||
</dependency>
|
||||
-->
|
||||
<!-- net clients
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.5.13</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<groupId>commons-logging</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpmime</artifactId>
|
||||
<version>4.5.13</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-net</groupId>
|
||||
<artifactId>commons-net</artifactId>
|
||||
<version>3.8.0</version>
|
||||
</dependency>
|
||||
-->
|
||||
<!-- pdf -->
|
||||
<dependency>
|
||||
<groupId>org.apache.pdfbox</groupId>
|
||||
<artifactId>pdfbox</artifactId>
|
||||
<version>2.0.28</version>
|
||||
</dependency>
|
||||
<!-- server-side events -->
|
||||
<dependency>
|
||||
<groupId>com.republicate</groupId>
|
||||
<artifactId>jeasse-servlet3</artifactId>
|
||||
<version>1.2</version>
|
||||
</dependency>
|
||||
<!-- graph solver -->
|
||||
<dependency>
|
||||
<groupId>org.jgrapht</groupId>
|
||||
<artifactId>jgrapht-core</artifactId>
|
||||
<version>1.5.2</version>
|
||||
</dependency>
|
||||
<!-- tests -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit.jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito.kotlin</groupId>
|
||||
<artifactId>mockito-kotlin</artifactId>
|
||||
<version>4.1.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- test emails -->
|
||||
<dependency>
|
||||
<groupId>com.icegreen</groupId>
|
||||
<artifactId>greenmail</artifactId>
|
||||
<version>1.6.12</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>javax.activation</groupId>
|
||||
<artifactId>activation</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
<artifactId>javax.mail</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
5
api-webapp/src/main/config/jetty/jetty-env.xml
Normal file
5
api-webapp/src/main/config/jetty/jetty-env.xml
Normal 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>
|
||||
|
26
api-webapp/src/main/config/jetty/jetty-http.xml
Normal file
26
api-webapp/src/main/config/jetty/jetty-http.xml
Normal 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>
|
24
api-webapp/src/main/config/jetty/jetty-https.xml
Normal file
24
api-webapp/src/main/config/jetty/jetty-https.xml
Normal 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>
|
16
api-webapp/src/main/config/jetty/jetty-ssl-context.xml
Normal file
16
api-webapp/src/main/config/jetty/jetty-ssl-context.xml
Normal 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>
|
46
api-webapp/src/main/config/jetty/jetty-ssl.xml
Normal file
46
api-webapp/src/main/config/jetty/jetty-ssl.xml
Normal 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>
|
8
api-webapp/src/main/config/webapp.properties
Normal file
8
api-webapp/src/main/config/webapp.properties
Normal file
@@ -0,0 +1,8 @@
|
||||
# webapp
|
||||
webapp.env = ${webapp.env}
|
||||
webapp.url = ${webapp.url}
|
||||
|
||||
# smtp
|
||||
|
||||
# Logging
|
||||
logger.level = ${logger.level}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
package org.jeudego.pairgoth.api
|
||||
|
||||
object StandingsHandler: PairgothApiHandler {
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
286
api-webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt
Normal file
286
api-webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt
Normal 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
|
||||
}
|
||||
}
|
@@ -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)
|
@@ -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
|
||||
)
|
@@ -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() })
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
@@ -0,0 +1,8 @@
|
||||
package org.jeudego.pairgoth.model
|
||||
|
||||
enum class Rules {
|
||||
FRENCH,
|
||||
JAPANESE,
|
||||
CHINESE
|
||||
// ...
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -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()
|
||||
)
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
@@ -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()
|
||||
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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())
|
||||
}
|
||||
}
|
@@ -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)
|
||||
|
||||
}
|
||||
}
|
@@ -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()
|
@@ -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<*>)
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -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
|
||||
}
|
||||
})
|
@@ -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)
|
||||
}
|
556
api-webapp/src/main/kotlin/org/jeudego/pairgoth/util/XmlUtils.kt
Normal file
556
api-webapp/src/main/kotlin/org/jeudego/pairgoth/util/XmlUtils.kt
Normal 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)
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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")
|
||||
}
|
||||
}
|
34
api-webapp/src/main/kotlin/org/jeudego/pairgoth/web/Event.kt
Normal file
34
api-webapp/src/main/kotlin/org/jeudego/pairgoth/web/Event.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
api-webapp/src/main/webapp/WEB-INF/jetty-web.xml
Normal file
10
api-webapp/src/main/webapp/WEB-INF/jetty-web.xml
Normal 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>
|
@@ -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
|
56
api-webapp/src/main/webapp/WEB-INF/web.xml
Normal file
56
api-webapp/src/main/webapp/WEB-INF/web.xml
Normal 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>
|
0
api-webapp/src/test/kotlin/.gitkeep
Normal file
0
api-webapp/src/test/kotlin/.gitkeep
Normal file
184
api-webapp/src/test/kotlin/BasicTests.kt
Normal file
184
api-webapp/src/test/kotlin/BasicTests.kt
Normal 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":"?"]"""
|
||||
}
|
||||
}
|
27
api-webapp/src/test/kotlin/ImportExportTests.kt
Normal file
27
api-webapp/src/test/kotlin/ImportExportTests.kt
Normal 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)+"...")
|
||||
}
|
||||
}
|
||||
}
|
24
api-webapp/src/test/kotlin/TestBase.kt
Normal file
24
api-webapp/src/test/kotlin/TestBase.kt
Normal 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 =====")
|
||||
}
|
||||
}
|
98
api-webapp/src/test/kotlin/TestUtils.kt
Normal file
98
api-webapp/src/test/kotlin/TestUtils.kt
Normal 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()
|
194
api-webapp/src/test/resources/opengotha/frioul-2018.xml
Normal file
194
api-webapp/src/test/resources/opengotha/frioul-2018.xml
Normal 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>
|
184
api-webapp/src/test/resources/opengotha/veterans-2021.xml
Normal file
184
api-webapp/src/test/resources/opengotha/veterans-2021.xml
Normal 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>
|
Reference in New Issue
Block a user