Jetty embedding revisited

This commit is contained in:
Claude Brisson
2023-05-12 14:21:41 +02:00
parent adca33ee26
commit a28f623eee
45 changed files with 911 additions and 470 deletions

204
webapp/pom.xml Normal file
View File

@@ -0,0 +1,204 @@
<?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>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>
</plugins>
</build>
<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.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.6.0</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.3.3</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.5</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>2.1.23</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>1.6</version>
<scope>runtime</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>
<!-- 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,55 @@
<?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 mappings -->
<servlet-mapping>
<servlet-name>api</servlet-name>
<url-pattern>/api/*</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>
<context-param>
<param-name>webapp-slf4j-logger.level</param-name>
<param-value>${logger.level}</param-value>
</context-param>
<context-param>
<param-name>webapp-slf4j-logger.notification</param-name>
<param-value>${logger.notification}</param-value>
</context-param>
</web-app>

View File

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

View File

@@ -0,0 +1,54 @@
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) =
when (request.method) {
"GET" -> get(request)
"POST" -> post(request)
"PUT" -> put(request)
"DELETE" -> delete(request)
else -> notImplemented()
}
fun get(request: HttpServletRequest): 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 getSelector(request: HttpServletRequest): String? {
return request.getAttribute(SELECTOR_KEY) as String?
}
companion object {
const val PAYLOAD_KEY = "PAYLOAD"
const val SELECTOR_KEY = "SELECTOR"
val logger = LoggerFactory.getLogger("api")
fun badRequest(msg: String = "bad request"): Nothing = throw ApiException(HttpServletResponse.SC_BAD_REQUEST, msg)
}
}

View File

@@ -0,0 +1,4 @@
package org.jeudego.pairgoth.api
class PlayerHandler: ApiHandler {
}

View File

@@ -0,0 +1,44 @@
package org.jeudego.pairgoth.api
import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.CanadianByoyomi
import org.jeudego.pairgoth.model.FisherTime
import org.jeudego.pairgoth.model.MacMahon
import org.jeudego.pairgoth.model.Rules
import org.jeudego.pairgoth.model.StandardByoyomi
import org.jeudego.pairgoth.model.SuddenDeath
import org.jeudego.pairgoth.model.TimeSystem
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.TournamentType
import org.jeudego.pairgoth.model.fromJson
import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.store.Store
import javax.servlet.http.HttpServletRequest
class TournamentHandler(): ApiHandler {
override fun post(request: HttpServletRequest): Json {
val json = getPayload(request)
if (!json.isObject) badRequest("expecting a json object")
val payload = json.asObject()
// tournament parsing
val tournament = Tournament.fromJson(payload)
Store.addTournament(tournament)
return Json.Object("success" to true, "id" to tournament.id)
}
override fun get(request: HttpServletRequest): Json {
return when (val id = getSelector(request)?.toIntOrNull()) {
null -> Json.Array(Store.getTournamentsIDs())
else -> Store.getTournament(id)?.toJson() ?: badRequest("no tournament with id #${id}")
}
}
override fun put(request: HttpServletRequest): Json {
val id = getSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid tournament selector")
TODO()
}
}

View File

@@ -0,0 +1,24 @@
package org.jeudego.pairgoth.model
sealed class Pairable(val id: Int, val name: String, val rating: Double, val rank: Int)
fun Pairable.displayRank(): String = when {
rank < 0 -> "${-rank}k"
rank >= 0 && rank < 10 -> "${rank + 1}d"
rank >= 10 -> "${rank - 9}p"
else -> throw Error("impossible")
}
private val rankRegex = Regex("(\\d+)([kdp])", RegexOption.IGNORE_CASE)
fun Pairable.setRank(rankStr: String): Int {
val (level, letter) = rankRegex.matchEntire(rankStr)?.destructured ?: throw Error("invalid rank: $rankStr")
val num = level.toInt()
if (num < 0 || num > 9) throw Error("invalid rank: $rankStr")
return when (letter.lowercase()) {
"k" -> -num
"d" -> num - 1
"p" -> num + 9
else -> throw Error("impossible")
}
}

View File

@@ -0,0 +1,48 @@
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.*
// TODO - this is only an early draft
sealed class Pairing(val type: PairingType) {
companion object {}
enum class PairingType { SWISS, MACMAHON, ROUNDROBIN }
}
class Swiss(
var method: Method,
var firstRoundMethod: Method = method
): Pairing(SWISS) {
enum class Method { FOLD, RANDOM, SLIP }
}
class MacMahon(
var bar: Int = 0,
var minLevel: Int = -30
): Pairing(MACMAHON) {
val groups = mutableListOf<Int>()
}
class RoundRobin: Pairing(ROUNDROBIN)
// 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
)
ROUNDROBIN -> RoundRobin()
}
fun Pairing.toJson() = when (this) {
is Swiss -> 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)
is RoundRobin -> Json.Object("type" to type.name)
}

View File

@@ -0,0 +1,15 @@
package org.jeudego.pairgoth.model
class Player(
id: Int,
name: String,
var firstname: String,
rating: Double,
rank: Int,
var country: String,
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>()
}

View File

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

View File

@@ -0,0 +1,6 @@
package org.jeudego.pairgoth.model
class Team(id: Int, name: String, rating: Double, rank: Int): Pairable(id, name, rating, rank) {
companion object {}
val players = mutableSetOf<Player>()
}

View File

@@ -0,0 +1,92 @@
package org.jeudego.pairgoth.model
import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler
sealed 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, FISHER, SUDDEN_DEATH }
}
class CanadianByoyomi(mainTime: Int, byoyomi: Int, stones: Int):
TimeSystem(
type = TimeSystemType.CANADIAN,
mainTime = mainTime,
increment = 0,
byoyomi = byoyomi,
periods = 1,
stones = stones
)
class StandardByoyomi(mainTime: Int, byoyomi: Int, periods: Int):
TimeSystem(
type = TimeSystemType.STANDARD,
mainTime = mainTime,
increment = 0,
byoyomi = byoyomi,
periods = periods,
stones = 1
)
class FisherTime(mainTime: Int, increment: Int, maxTime: Int = Int.MAX_VALUE):
TimeSystem(
type = TimeSystemType.FISHER,
mainTime = mainTime,
increment = increment,
maxTime = maxTime,
byoyomi = 0,
periods = 0,
stones = 0
)
class SuddenDeath(mainTime: Int):
TimeSystem(
type = TimeSystemType.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() ?: ApiHandler.badRequest("missing timeSystem type")) {
"CANADIAN" -> CanadianByoyomi(
mainTime = json.getInt("mainTime") ?: ApiHandler.badRequest("missing timeSystem mainTime"),
byoyomi = json.getInt("byoyomi") ?: ApiHandler.badRequest("missing timeSystem byoyomi"),
stones = json.getInt("stones") ?: ApiHandler.badRequest("missing timeSystem stones")
)
"STANDARD" -> StandardByoyomi(
mainTime = json.getInt("mainTime") ?: ApiHandler.badRequest("missing timeSystem mainTime"),
byoyomi = json.getInt("byoyomi") ?: ApiHandler.badRequest("missing timeSystem byoyomi"),
periods = json.getInt("periods") ?: ApiHandler.badRequest("missing timeSystem periods")
)
"FISHER" -> FisherTime(
mainTime = json.getInt("mainTime") ?: ApiHandler.badRequest("missing timeSystem mainTime"),
increment = json.getInt("increment") ?: ApiHandler.badRequest("missing timeSystem increment"),
maxTime = json.getInt("increment") ?: Integer.MAX_VALUE
)
"SUDDEN_DEATH" -> SuddenDeath(
mainTime = json.getInt("mainTime") ?: ApiHandler.badRequest("missing timeSystem mainTime"),
)
else -> ApiHandler.badRequest("invalid or missing timeSystem type")
}
fun TimeSystem.toJson() = when (type) {
TimeSystem.TimeSystemType.CANADIAN -> Json.Object("mainTime" to mainTime, "byoyomi" to byoyomi, "stones" to stones)
TimeSystem.TimeSystemType.STANDARD -> Json.Object("mainTime" to mainTime, "byoyomi" to byoyomi, "periods" to periods)
TimeSystem.TimeSystemType.FISHER -> Json.Object("mainTime" to mainTime, "increment" to increment, "maxTime" to maxTime)
TimeSystem.TimeSystemType.SUDDEN_DEATH -> Json.Object("mainTime" to mainTime)
}

View File

@@ -0,0 +1,74 @@
package org.jeudego.pairgoth.model
import com.republicate.kson.Json
import kotlinx.datetime.LocalDate
import org.jeudego.pairgoth.api.ApiHandler
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.store.Store
enum class TournamentType(val playersNumber: Int) {
INDIVIDUAL(1),
PAIRGO(2),
RENGO2(2),
RENGO3(3),
TEAM2(2),
TEAM3(3),
TEAM4(4),
TEAM5(5);
}
data class Tournament(
var id: Int,
var type: TournamentType,
var name: String,
var shortName: String,
var startDate: LocalDate,
var endDate: LocalDate,
var country: String,
var location: String,
var online: Boolean,
var timeSystem: TimeSystem,
var pairing: Pairing,
var rules: Rules = Rules.FRENCH,
var gobanSize: Int = 19,
var komi: Double = 7.5
) {
companion object {}
val pairables = mutableMapOf<Int, Pairable>()
}
// Serialization
fun Tournament.Companion.fromJson(json: Json.Object) = Tournament(
id = json.getInt("id") ?: Store.nextTournamentId,
type = json.getString("type")?.uppercase()?.let { TournamentType.valueOf(it) } ?: badRequest("missing type"),
name = json.getString("name") ?: ApiHandler.badRequest("missing name"),
shortName = json.getString("shortName") ?: ApiHandler.badRequest("missing shortName"),
startDate = json.getLocalDate("startDate") ?: ApiHandler.badRequest("missing startDate"),
endDate = json.getLocalDate("endDate") ?: ApiHandler.badRequest("missing endDate"),
country = json.getString("country") ?: ApiHandler.badRequest("missing country"),
location = json.getString("location") ?: ApiHandler.badRequest("missing location"),
online = json.getBoolean("online") ?: false,
komi = json.getDouble("komi") ?: 7.5,
rules = json.getString("rules")?.let { Rules.valueOf(it) } ?: Rules.FRENCH,
gobanSize = json.getInt("gobanSize") ?: 19,
timeSystem = TimeSystem.fromJson(json.getObject("timeSystem") ?: badRequest("missing timeSystem")),
pairing = MacMahon()
)
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(),
"pairing" to pairing.toJson()
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
package org.jeudego.pairgoth.store
import org.jeudego.pairgoth.model.Player
import org.jeudego.pairgoth.model.Tournament
import java.util.concurrent.atomic.AtomicInteger
object Store {
private val _nextTournamentId = AtomicInteger()
private val _nextPlayerId = AtomicInteger()
val nextTournamentId get() = _nextTournamentId.incrementAndGet()
val nextPlayerId get() = _nextPlayerId.incrementAndGet()
private val tournaments = mutableMapOf<Int, Tournament>()
private val players = mutableMapOf<Int, Player>()
fun addTournament(tournament: Tournament) {
if (tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} already exists")
tournaments[tournament.id] = tournament
}
fun getTournament(id: Int) = tournaments[id]
fun getTournamentsIDs(): Set<Int> = tournaments.keys
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,223 @@
package org.jeudego.pairgoth.web
import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler
import org.jeudego.pairgoth.api.PlayerHandler
import org.jeudego.pairgoth.api.TournamentHandler
import org.jeudego.pairgoth.util.Colorizer
import org.jeudego.pairgoth.util.Colorizer.green
import org.jeudego.pairgoth.util.Colorizer.red
import org.jeudego.pairgoth.util.parse
import org.jeudego.pairgoth.util.toString
import org.jeudego.pairgoth.web.ApiException
import org.slf4j.LoggerFactory
import java.io.IOException
import java.io.StringWriter
import java.util.*
import javax.servlet.ServletException
import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
class ApiServlet : HttpServlet() {
val tournamentHandler = TournamentHandler()
val playerHandler = PlayerHandler()
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 fun doRequest(request: HttpServletRequest, response: HttpServletResponse) {
val uri = request.requestURI
logger.logRequest(request, !uri.contains(".") && uri.length > 1)
var payload: Json? = null
var reason = "OK"
try {
if ("dev" == WebappManager.getProperty("webapp.env")) {
response.addHeader("Access-Control-Allow-Origin", "*")
}
validateContentType(request)
validateAccept(request);
val parts = uri.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (parts.size < 3 || parts.size > 5) throw ApiException(HttpServletResponse.SC_BAD_REQUEST)
if (parts.size >= 4) {
request.setAttribute(ApiHandler.SELECTOR_KEY, parts[3])
}
val entity = parts[2]
val handler = when (entity) {
"tournament" -> tournamentHandler
"player" -> playerHandler
else -> ApiHandler.badRequest("unknown entity")
}
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"
if (reason == null) error(response, apiException.code) else 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.info(red(">> {}"), builder.toString())
} else {
logger.info(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(green(">> "), 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)) throw ApiException(
HttpServletResponse.SC_BAD_REQUEST,
"JSON content expected"
)
// 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)
}
}
@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)) 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 {
protected var logger = LoggerFactory.getLogger("api")
protected const val EXPECTED_CHARSET = "utf8"
const val AUTH_HEADER = "Authorization"
const val AUTH_PREFIX = "Bearer"
private fun isJson(mimeType: String): Boolean {
return "text/json" == mimeType || "application/json" == mimeType ||
mimeType.endsWith("+json")
}
}
}

View File

@@ -0,0 +1,71 @@
package org.jeudego.pairgoth.web
import com.republicate.kson.Json
import org.jeudego.pairgoth.util.Colorizer.blue
import org.jeudego.pairgoth.util.Colorizer.green
import org.jeudego.pairgoth.util.toString
import org.slf4j.Logger
import java.io.StringWriter
import javax.servlet.http.HttpServletRequest
fun Logger.logRequest(req: HttpServletRequest, logHeaders: Boolean = false) {
val builder = StringBuilder()
builder.append(req.method).append(' ')
.append(req.scheme).append("://")
.append(req.localName)
val port = req.localPort
if (port != 80) builder.append(':').append(port)
if (!req.contextPath.isEmpty()) {
builder.append(req.contextPath)
}
builder.append(req.requestURI)
if (req.method == "GET") {
val qs = req.queryString
if (qs != null) builder.append('?').append(qs)
}
// builder.append(' ').append(req.getProtocol());
info(blue("<< {}"), builder.toString())
if (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)
info(blue("<< {}: {}"), name, value)
}
}
}
fun Logger.logPayload(prefix: String?, payload: Json, upstream: Boolean) {
val writer = StringWriter()
//payload.toPrettyString(writer, "");
payload.toString(writer)
if (isTraceEnabled) {
for (line in writer.toString().split("\n".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()) {
trace(if (upstream) blue("{}{}") else green("{}{}"), prefix, line)
}
} else {
var line = writer.toString()
val pos = line.indexOf('\n')
if (pos != -1) line = line.substring(0, pos)
if (line.length > 50) line = line.substring(0, 50) + "..."
debug(if (upstream) blue("{}{}") else green("{}{}"), prefix, line)
}
}
fun HttpServletRequest.getRemoteAddress(): String? {
var ip = getHeader("X-Forwarded-For")
if (ip == null) {
ip = remoteAddr
} else {
val comma = ip.indexOf(',')
if (comma != -1) {
ip = ip.substring(0, comma).trim { it <= ' ' } // keep the left-most IP address
}
}
return ip
}

View File

@@ -0,0 +1,184 @@
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.security.SecureRandom
import java.security.cert.X509Certificate
import java.util.*
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) {
// overcome a Jetty's bug (v9.4.10.v20180503) whereas if a @WebListener is also listed in the descriptor
// it will be instanciated twice...
context = sce.servletContext
logger.info("---------- Starting Web Application ----------")
context.setAttribute("manager", this)
webappRoot = context.getRealPath("/")
try {
properties.load(context.getResourceAsStream("/WEB-INF/webapp.properties"))
val submaps: MutableMap<String, MutableMap<String, String>> = HashMap()
for (prop in properties.stringPropertyNames()) {
val value = properties.getProperty(prop)
// filter out missing values and passwords
if (value.startsWith("\${") || prop.contains("password")) continue
context.setAttribute(prop, value)
// also support one level of submaps (TODO - more)
val dot = prop.indexOf('.')
if (dot != -1) {
val topKey = prop.substring(0, dot)
val subKey = prop.substring(dot + 1)
if ("password" == subKey) continue
var submap = submaps[topKey]
if (submap == null) {
submap = HashMap()
submaps[topKey] = submap
}
submap[subKey] = value
}
}
for ((key, value) in submaps) {
context.setAttribute(key, 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 ----------")
// overcome a Jetty's bug (v9.4.10.v20180503) whereas if a @WebListener is also listed in the descriptor
// it will be instanciated twice...
if (context == null) return
val context = sce.servletContext
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 {
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)
}
val webappURL by lazy { getProperty("webapp.url") }
private val services = mutableMapOf<String, Pair<Runnable, Thread>>()
@JvmOverloads
fun registerService(name: String?, task: Runnable, initialStatus: Boolean? = null) {
if (webServices.containsKey(name)) {
logger.warn("service {} already registered")
return
}
logger.debug("registered service {}", name)
webServices[name] =
Pair.of(task, null)
}
fun startService(name: String?) {
val service = webServices[name]!!
if (service.right != null && service.right!!.isAlive) {
logger.warn("service {} is already running", name)
return
}
logger.debug("starting service {}", name)
val thread = Thread(service.left, name)
thread.start()
webServices[name] =
Pair.of(
service.left,
thread
)
}
@JvmOverloads
fun stopService(name: String?, webappClosing: Boolean = false) {
val service = webServices[name]!!
val thread = service.right
if (thread == null || !thread.isAlive) {
logger.warn("service {} is already stopped", name)
return
}
logger.debug("stopping service {}", name)
thread.interrupt()
try {
thread.join()
} catch (ie: InterruptedException) {
}
if (!webappClosing) {
webServices[name] = Pair.of(service.left, null)
}
}
}
}

View File

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

View File

@@ -0,0 +1,2 @@
format = %date [%level] %ip [%logger] %message (@%file:%line:%column)
level = trace

View File

@@ -0,0 +1 @@
../../../../target/pairgoth-1.0-SNAPSHOT/WEB-INF/web.xml

View File

@@ -0,0 +1 @@
../../../../target/pairgoth-1.0-SNAPSHOT/WEB-INF/webapp.properties

View File