Various debugging; OpenGotha import draft
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
package org.jeudego.pairgoth.api
|
package org.jeudego.pairgoth.api
|
||||||
|
|
||||||
import com.republicate.kson.Json
|
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.api.ApiHandler.Companion.badRequest
|
||||||
|
import org.jeudego.pairgoth.ext.OpenGotha
|
||||||
import org.jeudego.pairgoth.model.Tournament
|
import org.jeudego.pairgoth.model.Tournament
|
||||||
import org.jeudego.pairgoth.model.fromJson
|
import org.jeudego.pairgoth.model.fromJson
|
||||||
import org.jeudego.pairgoth.model.toJson
|
import org.jeudego.pairgoth.model.toJson
|
||||||
import org.jeudego.pairgoth.store.Store
|
import org.jeudego.pairgoth.store.Store
|
||||||
|
import org.w3c.dom.Element
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest
|
||||||
|
|
||||||
object TournamentHandler: PairgothApiHandler {
|
object TournamentHandler: PairgothApiHandler {
|
||||||
@@ -18,10 +21,11 @@ object TournamentHandler: PairgothApiHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun post(request: HttpServletRequest): Json {
|
override fun post(request: HttpServletRequest): Json {
|
||||||
val payload = getObjectPayload(request)
|
val tournament = when (val payload = request.getAttribute(PAYLOAD_KEY)) {
|
||||||
|
is Json.Object -> Tournament.fromJson(getObjectPayload(request))
|
||||||
// tournament parsing
|
is Element -> OpenGotha.import(payload)
|
||||||
val tournament = Tournament.fromJson(payload)
|
else -> badRequest("missing or invalid payload")
|
||||||
|
}
|
||||||
|
|
||||||
Store.addTournament(tournament)
|
Store.addTournament(tournament)
|
||||||
return Json.Object("success" to true, "id" to tournament.id)
|
return Json.Object("success" to true, "id" to tournament.id)
|
||||||
|
176
webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt
Normal file
176
webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
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.SuddenDeath
|
||||||
|
import org.jeudego.pairgoth.model.Swiss
|
||||||
|
import org.jeudego.pairgoth.model.Tournament
|
||||||
|
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 = Tournament(
|
||||||
|
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.pairables) { 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()
|
||||||
|
}
|
||||||
|
tournament.games.addAll(gamesPerRound)
|
||||||
|
return tournament
|
||||||
|
}
|
||||||
|
}
|
@@ -10,7 +10,7 @@ data class Game(
|
|||||||
val handicap: Int = 0,
|
val handicap: Int = 0,
|
||||||
var result: Result = UNKNOWN
|
var result: Result = UNKNOWN
|
||||||
) {
|
) {
|
||||||
enum class Result(val symbol: Char) { UNKNOWN('?'), BLACK('b'), WHITE('w'), JIGO('='), CANCELLED('x') }
|
enum class Result(val symbol: Char) { UNKNOWN('?'), BLACK('b'), WHITE('w'), JIGO('='), CANCELLED('x'), BOTHWIN('+'), BOTHLOOSE('-') }
|
||||||
}
|
}
|
||||||
|
|
||||||
// serialization
|
// serialization
|
||||||
|
@@ -10,6 +10,7 @@ import kotlin.math.roundToInt
|
|||||||
// Pairable
|
// Pairable
|
||||||
|
|
||||||
sealed class Pairable(val id: Int, val name: String, open val rating: Int, open val rank: Int) {
|
sealed class Pairable(val id: Int, val name: String, open val rating: Int, open val rank: Int) {
|
||||||
|
companion object {}
|
||||||
abstract fun toJson(): Json.Object
|
abstract fun toJson(): Json.Object
|
||||||
val skip = mutableSetOf<Int>() // skipped rounds
|
val skip = mutableSetOf<Int>() // skipped rounds
|
||||||
}
|
}
|
||||||
@@ -29,10 +30,10 @@ fun Pairable.displayRank(): String = when {
|
|||||||
|
|
||||||
private val rankRegex = Regex("(\\d+)([kdp])", RegexOption.IGNORE_CASE)
|
private val rankRegex = Regex("(\\d+)([kdp])", RegexOption.IGNORE_CASE)
|
||||||
|
|
||||||
fun Pairable.setRank(rankStr: String): Int {
|
fun Pairable.Companion.parseRank(rankStr: String): Int {
|
||||||
val (level, letter) = rankRegex.matchEntire(rankStr)?.destructured ?: throw Error("invalid rank: $rankStr")
|
val (level, letter) = rankRegex.matchEntire(rankStr)?.destructured ?: throw Error("invalid rank: $rankStr")
|
||||||
val num = level.toInt()
|
val num = level.toInt()
|
||||||
if (num < 0 || num > 9) throw Error("invalid rank: $rankStr")
|
if (num < 0 || letter != "k" && letter != "K" && num > 9) throw Error("invalid rank: $rankStr")
|
||||||
return when (letter.lowercase()) {
|
return when (letter.lowercase()) {
|
||||||
"k" -> -num
|
"k" -> -num
|
||||||
"d" -> num - 1
|
"d" -> num - 1
|
||||||
|
@@ -30,7 +30,7 @@ class SwissSolver(history: List<Game>, pairables: List<Pairable>, val method: Sw
|
|||||||
SPLIT_AND_FOLD ->
|
SPLIT_AND_FOLD ->
|
||||||
if (p.placeInGroup.first > q.placeInGroup.first) abs(p.placeInGroup.first - (q.placeInGroup.second - q.placeInGroup.first)) * PLACE_WEIGHT
|
if (p.placeInGroup.first > q.placeInGroup.first) abs(p.placeInGroup.first - (q.placeInGroup.second - q.placeInGroup.first)) * PLACE_WEIGHT
|
||||||
else abs(q.placeInGroup.first - (p.placeInGroup.second - p.placeInGroup.first)) * PLACE_WEIGHT
|
else abs(q.placeInGroup.first - (p.placeInGroup.second - p.placeInGroup.first)) * PLACE_WEIGHT
|
||||||
SPLIT_AND_RANDOM -> rand.nextDouble(p.placeInGroup.second.toDouble()) * PLACE_WEIGHT
|
SPLIT_AND_RANDOM -> rand.nextDouble() * p.placeInGroup.second * PLACE_WEIGHT
|
||||||
SPLIT_AND_SLIP -> abs(abs(p.placeInGroup.first - q.placeInGroup.first) - p.placeInGroup.second) * PLACE_WEIGHT
|
SPLIT_AND_SLIP -> abs(abs(p.placeInGroup.first - q.placeInGroup.first) - p.placeInGroup.second) * PLACE_WEIGHT
|
||||||
else -> throw Error("unhandled case")
|
else -> throw Error("unhandled case")
|
||||||
}
|
}
|
||||||
|
362
webapp/src/main/kotlin/org/jeudego/pairgoth/util/XmlFormat.kt
Normal file
362
webapp/src/main/kotlin/org/jeudego/pairgoth/util/XmlFormat.kt
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
package org.jeudego.pairgoth.util
|
||||||
|
|
||||||
|
import com.republicate.kson.Json
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
import org.w3c.dom.ElementTraversal
|
||||||
|
import org.w3c.dom.Node
|
||||||
|
import org.w3c.dom.NodeList
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.DateTimeFormatterBuilder
|
||||||
|
import java.time.temporal.ChronoField
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
// "XMLFormat" xml parsing and formatting utility class
|
||||||
|
// It is currently being packaged as an external open source library.
|
||||||
|
// See opengotha import code as a self-documenting example for how to use this class
|
||||||
|
|
||||||
|
open class XmlFormat(val xml: Element)
|
||||||
|
|
||||||
|
// standard types delegates
|
||||||
|
|
||||||
|
fun XmlFormat.string() = StringXmlDelegate(xml.element())
|
||||||
|
fun XmlFormat.optString() = OptionalStringXmlDelegate(xml.element())
|
||||||
|
fun XmlFormat.boolean() = BooleanXmlDelegate(xml.element())
|
||||||
|
fun XmlFormat.optBoolean() = OptionalBooleanXmlDelegate(xml.element())
|
||||||
|
fun XmlFormat.int() = IntXmlDelegate(xml.element())
|
||||||
|
fun XmlFormat.optInt() = OptionalIntXmlDelegate(xml.element())
|
||||||
|
fun XmlFormat.long() = LongXmlDelegate(xml.element())
|
||||||
|
fun XmlFormat.optLong() = OptionalLongXmlDelegate(xml.element())
|
||||||
|
fun XmlFormat.double() = DoubleXmlDelegate(xml.element())
|
||||||
|
//fun XmlFormat.optDouble() = OptinalDoubleXmlDelegate(xml.element())
|
||||||
|
inline fun <reified E: Enum<*>> XmlFormat.enum() = EnumXmlDelegate<E>(xml.element(), E::class)
|
||||||
|
fun XmlFormat.date(format: String = ISO_LOCAL_DATE_FORMAT) = DateXmlDelegate(xml.element(), format)
|
||||||
|
fun XmlFormat.datetime(format: String = ISO_LOCAL_DATETIME_FORMAT) = DateTimeXmlDelegate(xml.element(), format)
|
||||||
|
inline fun <reified T: XmlFormat> XmlFormat.childrenArrayOf() = ArrayXmlDelegate(xml, T::class)
|
||||||
|
inline fun <reified T: XmlFormat> XmlFormat.childrenArrayOf(tagName: String) = ChildrenArrayXmlDelegate(xml, tagName, T::class)
|
||||||
|
inline fun <reified T: XmlFormat> XmlFormat.mutableArrayOf() = MutableArrayXmlDelegate(xml, T::class)
|
||||||
|
inline fun <reified T: XmlFormat> XmlFormat.objectOf() = ObjectXmlDelegate(xml, T::class)
|
||||||
|
|
||||||
|
// standard type delegates for attributes
|
||||||
|
|
||||||
|
fun XmlFormat.stringAttr() = StringXmlAttrDelegate(xml.element())
|
||||||
|
fun XmlFormat.booleanAttr() = BooleanXmlAttrDelegate(xml.element())
|
||||||
|
fun XmlFormat.intAttr() = IntXmlAttrDelegate(xml.element())
|
||||||
|
fun XmlFormat.longAttr() = LongXmlAttrDelegate(xml.element())
|
||||||
|
fun XmlFormat.doubleAttr() = DoubleXmlAttrDelegate(xml.element())
|
||||||
|
fun XmlFormat.dateAttr(format: String = ISO_LOCAL_DATE_FORMAT) = DateXmlAttrDelegate(xml.element(), format)
|
||||||
|
|
||||||
|
// xpath delegates
|
||||||
|
|
||||||
|
fun XmlFormat.string(xpath: String) = StringXmlAttrDelegate(xml.element().find(xpath)[0].element())
|
||||||
|
fun XmlFormat.boolean(xpath: String) = StringXmlAttrDelegate(xml.element().find(xpath)[0].element())
|
||||||
|
fun XmlFormat.int(xpath: String) = StringXmlAttrDelegate(xml.element().find(xpath)[0].element())
|
||||||
|
fun XmlFormat.long(xpath: String) = StringXmlAttrDelegate(xml.element().find(xpath)[0].element())
|
||||||
|
|
||||||
|
// Helper classes and functions
|
||||||
|
|
||||||
|
private fun error(propName: String): Nothing { throw Error("missing property $propName") }
|
||||||
|
|
||||||
|
open class OptionalStringXmlDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): String? = xml.childOrNull(property.name)?.value()
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) { value?.let { xml.child(property.name).textContent = value } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class StringXmlDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): String = xml.childOrNull(property.name)?.value() ?: error(property.name)
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { value?.let { xml.child(property.name).textContent = value } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class OptionalBooleanXmlDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Boolean? = Json.TypeUtils.toBoolean(xml.childOrNull(property.name)?.value())
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean?) { value?.let { xml.child(property.name).textContent = value.toString() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class BooleanXmlDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Boolean = Json.TypeUtils.toBoolean(xml.childOrNull(property.name)?.value()) ?: error(property.name)
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) { value?.let { xml.child(property.name).textContent = value.toString() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class OptionalIntXmlDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Int? = Json.TypeUtils.toInt(xml.childOrNull(property.name)?.value())
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int?) { value?.let { xml.child(property.name).textContent = value.toString() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class IntXmlDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Int = Json.TypeUtils.toInt(xml.childOrNull(property.name)?.value()) ?: error(property.name)
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { value?.let { xml.child(property.name).textContent = value.toString() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class OptionalLongXmlDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Long? = Json.TypeUtils.toLong(xml.childOrNull(property.name)?.value())
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Long?) { value?.let { xml.child(property.name).textContent = value.toString() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class LongXmlDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Long = Json.TypeUtils.toLong(xml.childOrNull(property.name)?.value()) ?: error(property.name)
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Long) { value?.let { xml.child(property.name).textContent = value.toString() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class OptionalDoubleXmlDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Double? = Json.TypeUtils.toDouble(xml.childOrNull(property.name)?.value())
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Double?) { value?.let { xml.child(property.name).textContent = value.toString() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class DoubleXmlDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Double = Json.TypeUtils.toDouble(xml.childOrNull(property.name)?.value()) ?: error(property.name)
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Double) { value?.let { xml.child(property.name).textContent = value.toString() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class OptionalEnumXmlDelegate<E: Enum<*>> (val xml: Element, private val kclass: KClass<E>) {
|
||||||
|
operator fun getValue(thisRef: Any?, property: KProperty<*>): E? {
|
||||||
|
val enumValues = kclass.java.enumConstants as Array<E>
|
||||||
|
val xmlValue = xml.childOrNull(property.name)?.textContent
|
||||||
|
return enumValues.firstOrNull() { it.name == xmlValue }
|
||||||
|
}
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: E?) { value?.let { xml.child(property.name).textContent = value.toString() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class EnumXmlDelegate<E: Enum<*>> (val xml: Element, private val kclass: KClass<E>) {
|
||||||
|
operator fun getValue(thisRef: Any?, property: KProperty<*>): E {
|
||||||
|
val enumValues = kclass.java.enumConstants as Array<E>
|
||||||
|
val xmlValue = xml.childOrNull(property.name)?.textContent
|
||||||
|
return enumValues.firstOrNull() { it.name == xmlValue } ?: error(property.name)
|
||||||
|
}
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: E) { value?.let { xml.child(property.name).textContent = value.toString() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
const val ISO_LOCAL_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"
|
||||||
|
const val ISO_LOCAL_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"
|
||||||
|
const val ISO_UTC_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||||
|
const val ISO_ZONED_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssXXX"
|
||||||
|
const val ISO_LOCAL_YMD = "yyyyMMdd"
|
||||||
|
const val ISO_LOCAL_YMDHM = "yyyyMMddHHmm"
|
||||||
|
const val LOCAL_FRENCHY = "dd/MM/yyyy HH:mm"
|
||||||
|
|
||||||
|
internal fun dateTimeFormat(format: String): DateTimeFormatter {
|
||||||
|
val builder = DateTimeFormatterBuilder()
|
||||||
|
if (format.startsWith("yyyy")) {
|
||||||
|
// workaround Java bug
|
||||||
|
builder.appendValue(ChronoField.YEAR_OF_ERA, 4)
|
||||||
|
.appendPattern(format.substring(4))
|
||||||
|
} else {
|
||||||
|
builder.appendPattern(format)
|
||||||
|
}
|
||||||
|
builder.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
|
||||||
|
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
|
||||||
|
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
|
||||||
|
return builder.toFormatter()
|
||||||
|
}
|
||||||
|
|
||||||
|
open class OptionalDateXmlDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATE_FORMAT) {
|
||||||
|
private val format = dateTimeFormat(formatString)
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDate? = xml.childOrNull(property.name)?.value()?.let { LocalDate.parse(it /*, format */) }
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDate?) { value?.let { xml.child(property.name).textContent = value?.let { /* format.format(value)*/ value.toString() } } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class DateXmlDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATE_FORMAT) {
|
||||||
|
private val format = dateTimeFormat(formatString)
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDate = xml.childOrNull(property.name)?.value()?.let { LocalDate.parse(it /*, format */) } ?: error(property.name)
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDate) { value?.let { xml.child(property.name).textContent = value?.let { /* format.format(value)*/ value.toString() } } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class OptionalDateTimeXmlDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATETIME_FORMAT) {
|
||||||
|
private val format = dateTimeFormat(formatString)
|
||||||
|
//FB TODO ** To rewrite
|
||||||
|
//open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDateTime? = xml.childOrNull(property.name)?.value()?.let { LocalDateTime.parse(it, format) }
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDateTime? {
|
||||||
|
var inputString : String? = xml.childOrNull(property.name)?.value()?.replace("_","0")
|
||||||
|
if (inputString?.length == 16) inputString = inputString.plus(":00")
|
||||||
|
return inputString?.let { LocalDateTime.parse(it /*, format*/) } // CB TODO format handling
|
||||||
|
}
|
||||||
|
//**
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDateTime?) { value?.let { xml.child(property.name).textContent = value?.let { value.toString() /* format.format(value)*/ } } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class DateTimeXmlDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATETIME_FORMAT) {
|
||||||
|
private val format = dateTimeFormat(formatString)
|
||||||
|
//FB TODO ** To rewrite
|
||||||
|
//open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDateTime? = xml.childOrNull(property.name)?.value()?.let { LocalDateTime.parse(it, format) }
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDateTime {
|
||||||
|
var inputString : String? = xml.childOrNull(property.name)?.value()?.replace("_","0")
|
||||||
|
if (inputString?.length == 16) inputString = inputString.plus(":00")
|
||||||
|
return inputString?.let { LocalDateTime.parse(it /*, format*/) } ?: error(property.name) // CB TODO format handling
|
||||||
|
}
|
||||||
|
//**
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDateTime) { value?.let { xml.child(property.name).textContent = value?.let { value.toString() /* format.format(value)*/ } } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <F: XmlFormat, T: Any> KClass<F>.instantiate(content: T): F = constructors.first().call(content)
|
||||||
|
|
||||||
|
open class ObjectXmlDelegate <T: XmlFormat> (val xml: Node, private val klass: KClass<T>) {
|
||||||
|
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||||
|
val obj = xml.element().child(property.name)
|
||||||
|
return obj.let {
|
||||||
|
klass.instantiate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// standard types attributes delegates
|
||||||
|
open class OptionalStringXmlAttrDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): String? = xml.attr(property.name)
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) { value?.let { xml.setAttr(property.name, value) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class StringXmlAttrDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): String = xml.attr(property.name) ?: error(property.name)
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { value?.let { xml.setAttr(property.name, value) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class OptionalBooleanXmlAttrDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Boolean? = xml.boolAttr(property.name)
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean?) { value?.let { xml.setAttr(property.name, value) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class BooleanXmlAttrDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Boolean = xml.boolAttr(property.name) ?: error(property.name)
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) { value?.let { xml.setAttr(property.name, value) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class OptionalIntXmlAttrDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Int? = xml.intAttr(property.name)
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int?) { value?.let { xml.setAttr(property.name, value) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class IntXmlAttrDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Int = xml.intAttr(property.name) ?: error(property.name)
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { value?.let { xml.setAttr(property.name, value) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class OptionalLongXmlAttrDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Long? = xml.longAttr(property.name)
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Long?) { value?.let { xml.setAttr(property.name, value) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class LongXmlAttrDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Long = xml.longAttr(property.name) ?: error(property.name)
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Long) { value?.let { xml.setAttr(property.name, value) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class OptionalDoubleXmlAttrDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Double? = xml.doubleAttr(property.name)
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Double?) { value?.let { xml.setAttr(property.name, value) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class DoubleXmlAttrDelegate(val xml: Element) {
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): Double = xml.doubleAttr(property.name) ?: error(property.name)
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Double) { value?.let { xml.setAttr(property.name, value) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class OptionalDateXmlAttrDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATE_FORMAT) {
|
||||||
|
private val format = dateTimeFormat(formatString)
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDate? = xml.attr(property.name)?.let { LocalDate.parse(it/*, format*/) }
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDate?) { value?.let { xml.setAttr(property.name, value.toString() /* format.format(value) */) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class DateXmlAttrDelegate(val xml: Element, formatString: String = ISO_LOCAL_DATE_FORMAT) {
|
||||||
|
private val format = dateTimeFormat(formatString)
|
||||||
|
open operator fun getValue(thisRef: Any?, property: KProperty<*>): LocalDate = xml.attr(property.name)?.let { LocalDate.parse(it/*, format*/) } ?: error(property.name)
|
||||||
|
open operator fun setValue(thisRef: Any?, property: KProperty<*>, value: LocalDate) { value?.let { xml.setAttr(property.name, value.toString() /* format.format(value) */) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// containers delegates
|
||||||
|
|
||||||
|
open class XmlArrayAdapter<T: XmlFormat>(val parent: Node, protected val functor: (Node)->T): List<T> {
|
||||||
|
override val size = (parent as ElementTraversal).childElementCount
|
||||||
|
override fun contains(element: T): Boolean { throw Error("not implemented") }
|
||||||
|
override fun containsAll(elements: Collection<T>): Boolean { throw Error("not implemented") }
|
||||||
|
override fun get(index: Int): T {
|
||||||
|
try {
|
||||||
|
return functor(parent.element().children()[index])
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Error("could not get child element", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun indexOf(element: T): Int { throw Error("not implemented") }
|
||||||
|
override fun isEmpty() = parent.childNodes.length == 0
|
||||||
|
override fun iterator(): Iterator<T> = object: Iterator<T> {
|
||||||
|
private val it = parent.element().children().iterator()
|
||||||
|
override fun hasNext() = it.hasNext()
|
||||||
|
override fun next() = functor(it.next())
|
||||||
|
}
|
||||||
|
override fun lastIndexOf(element: T): Int { throw Error("not implemented") }
|
||||||
|
override fun listIterator() = listIterator(0)
|
||||||
|
override fun listIterator(index: Int): ListIterator<T> { throw Error("not implemented") }
|
||||||
|
override fun subList(fromIndex: Int, toIndex: Int): List<T> { throw Error("not implemented") }
|
||||||
|
}
|
||||||
|
|
||||||
|
class MutableXmlArrayAdapter<T: XmlFormat>(parent: Node, functor: (Node)->T): XmlArrayAdapter<T>(parent, functor), MutableList<T> {
|
||||||
|
override fun iterator(): MutableIterator<T> = object: MutableIterator<T> {
|
||||||
|
private val it = parent.element().children().iterator()
|
||||||
|
override fun hasNext() = it.hasNext()
|
||||||
|
override fun next() = functor(it.next())
|
||||||
|
override fun remove() { throw Error("not implemented") }
|
||||||
|
}
|
||||||
|
override fun add(element: T): Boolean { parent.appendChild(element.xml); return true }
|
||||||
|
override fun add(index: Int, element: T) { throw Error("not implemented") }
|
||||||
|
override fun addAll(index: Int, elements: Collection<T>): Boolean { throw Error("not implemented") }
|
||||||
|
override fun addAll(elements: Collection<T>): Boolean { throw Error("not implemented") }
|
||||||
|
override fun clear() {
|
||||||
|
while (parent.firstChild != null) {
|
||||||
|
parent.removeChild(parent.firstChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun listIterator() = listIterator(0)
|
||||||
|
override fun listIterator(index: Int): MutableListIterator<T> { throw Error("not implemented") }
|
||||||
|
override fun remove(element: T): Boolean { throw Error("not implemented") }
|
||||||
|
override fun removeAll(elements: Collection<T>): Boolean { throw Error("not implemented") }
|
||||||
|
override fun removeAt(index: Int): T { throw Error("not implemented") }
|
||||||
|
override fun retainAll(elements: Collection<T>): Boolean { throw Error("not implemented") }
|
||||||
|
override fun set(index: Int, element: T): T { throw Error("not implemented") }
|
||||||
|
override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> {
|
||||||
|
throw NotImplementedError("Not implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class XmlArrayInlineAdapter<T: XmlFormat>(val list: NodeList, protected val functor: (Node)->T): List<T> {
|
||||||
|
override val size = list.length
|
||||||
|
override fun contains(element: T): Boolean { throw Error("not implemented") }
|
||||||
|
override fun containsAll(elements: Collection<T>): Boolean { throw Error("not implemented") }
|
||||||
|
override fun get(index: Int): T {
|
||||||
|
try {
|
||||||
|
return functor(list[index])
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Error("could not get child element", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun indexOf(element: T): Int { throw Error("not implemented") }
|
||||||
|
override fun isEmpty() = list.length == 0
|
||||||
|
override fun iterator(): Iterator<T> = object: Iterator<T> {
|
||||||
|
private val it = list.iterator()
|
||||||
|
override fun hasNext() = it.hasNext()
|
||||||
|
override fun next() = functor(it.next())
|
||||||
|
}
|
||||||
|
override fun lastIndexOf(element: T): Int { throw Error("not implemented") }
|
||||||
|
override fun listIterator() = listIterator(0)
|
||||||
|
override fun listIterator(index: Int): ListIterator<T> { throw Error("not implemented") }
|
||||||
|
override fun subList(fromIndex: Int, toIndex: Int): List<T> { throw Error("not implemented") }
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T: XmlFormat> MutableXmlArrayAdapter<T>.newChild(): T {
|
||||||
|
val node = parent.document().createElement(T::class.simpleName!!.lowercase())
|
||||||
|
// should be done explicitely in client code
|
||||||
|
// parent.element().appendChild(node)
|
||||||
|
return T::class.instantiate(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
open class ArrayXmlDelegate <T: XmlFormat> (val xml: Node, private val klass: KClass<T>) {
|
||||||
|
operator fun getValue(thisRef: Any?, property: KProperty<*>): List<T> =
|
||||||
|
XmlArrayAdapter<T>(xml.element().child(property.name), klass::instantiate)
|
||||||
|
}
|
||||||
|
|
||||||
|
open class MutableArrayXmlDelegate <T: XmlFormat> (val xml: Node, private val klass: KClass<T>) {
|
||||||
|
operator fun getValue(thisRef: Any?, property: KProperty<*>): MutableXmlArrayAdapter<T> =
|
||||||
|
MutableXmlArrayAdapter<T>(xml.element().child(property.name), klass::instantiate)
|
||||||
|
}
|
||||||
|
|
||||||
|
open class ChildrenArrayXmlDelegate <T: XmlFormat> (val xml: Node, private val tagName: String, private val klass: KClass<T>) {
|
||||||
|
operator fun getValue(thisRef: Any?, property: KProperty<*>): List<T> = XmlArrayInlineAdapter(xml.element().getElementsByTagName(tagName), klass::instantiate)
|
||||||
|
}
|
558
webapp/src/main/kotlin/org/jeudego/pairgoth/util/XmlUtils.kt
Normal file
558
webapp/src/main/kotlin/org/jeudego/pairgoth/util/XmlUtils.kt
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
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 {
|
||||||
|
var ret: NodeList? = null
|
||||||
|
val xp = XPathFactory.newInstance().newXPath()
|
||||||
|
val exp = xp.compile(xpath)
|
||||||
|
ret = exp.evaluate(context, XPathConstants.NODESET) as NodeList
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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? = null
|
||||||
|
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)
|
||||||
|
|
@@ -5,11 +5,14 @@ import org.jeudego.pairgoth.api.ApiHandler
|
|||||||
import org.jeudego.pairgoth.api.PairingHandler
|
import org.jeudego.pairgoth.api.PairingHandler
|
||||||
import org.jeudego.pairgoth.api.PlayerHandler
|
import org.jeudego.pairgoth.api.PlayerHandler
|
||||||
import org.jeudego.pairgoth.api.TournamentHandler
|
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.green
|
||||||
import org.jeudego.pairgoth.util.Colorizer.red
|
import org.jeudego.pairgoth.util.Colorizer.red
|
||||||
|
import org.jeudego.pairgoth.util.XmlUtils
|
||||||
import org.jeudego.pairgoth.util.parse
|
import org.jeudego.pairgoth.util.parse
|
||||||
import org.jeudego.pairgoth.util.toString
|
import org.jeudego.pairgoth.util.toString
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.w3c.dom.Element
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.locks.ReadWriteLock
|
import java.util.concurrent.locks.ReadWriteLock
|
||||||
@@ -60,8 +63,8 @@ class ApiServlet : HttpServlet() {
|
|||||||
if ("dev" == WebappManager.getProperty("webapp.env")) {
|
if ("dev" == WebappManager.getProperty("webapp.env")) {
|
||||||
response.addHeader("Access-Control-Allow-Origin", "*")
|
response.addHeader("Access-Control-Allow-Origin", "*")
|
||||||
}
|
}
|
||||||
validateContentType(request)
|
|
||||||
validateAccept(request);
|
validateAccept(request);
|
||||||
|
validateContentType(request)
|
||||||
|
|
||||||
// parse request URI
|
// parse request URI
|
||||||
|
|
||||||
@@ -169,22 +172,36 @@ class ApiServlet : HttpServlet() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// check content type
|
// check content type
|
||||||
if (!isJson(mimeType)) throw ApiException(
|
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,
|
HttpServletResponse.SC_BAD_REQUEST,
|
||||||
"JSON content expected"
|
"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)
|
@Throws(ApiException::class)
|
||||||
@@ -241,9 +258,7 @@ class ApiServlet : HttpServlet() {
|
|||||||
private const val EXPECTED_CHARSET = "utf8"
|
private const val EXPECTED_CHARSET = "utf8"
|
||||||
const val AUTH_HEADER = "Authorization"
|
const val AUTH_HEADER = "Authorization"
|
||||||
const val AUTH_PREFIX = "Bearer"
|
const val AUTH_PREFIX = "Bearer"
|
||||||
private fun isJson(mimeType: String): Boolean {
|
private fun isJson(mimeType: String) = "text/json" == mimeType || "application/json" == mimeType || mimeType.endsWith("+json")
|
||||||
return "text/json" == mimeType || "application/json" == mimeType ||
|
private fun isXml(mimeType: String) = "text/xml" == mimeType || "application/xml" == mimeType || mimeType.endsWith("+xml")
|
||||||
mimeType.endsWith("+json")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
webapp/src/test/kotlin/ImportTests.kt
Normal file
20
webapp/src/test/kotlin/ImportTests.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package org.jeudego.pairgoth.test
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
class ImportTests: 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")
|
||||||
|
logger.info(tournament.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -4,22 +4,21 @@ import org.junit.jupiter.api.BeforeAll
|
|||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.TestInfo
|
import org.junit.jupiter.api.TestInfo
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
abstract class TestBase {
|
abstract class TestBase {
|
||||||
companion object {
|
companion object {
|
||||||
val logger = LoggerFactory.getLogger("test")
|
val logger = LoggerFactory.getLogger("test")
|
||||||
private var testClassName: String? = null
|
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun prepare() {
|
fun prepare() {
|
||||||
testClassName = this::class.simpleName
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun before(testInfo: TestInfo) {
|
fun before(testInfo: TestInfo) {
|
||||||
val testName = testInfo.displayName.removeSuffix("()")
|
val testName = testInfo.displayName.removeSuffix("()")
|
||||||
logger.info("===== Running $testClassName.$testName =====")
|
logger.info("===== Running $testName =====")
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -6,25 +6,33 @@ import org.jeudego.pairgoth.web.ApiServlet
|
|||||||
import org.jeudego.pairgoth.web.WebappManager
|
import org.jeudego.pairgoth.web.WebappManager
|
||||||
import org.mockito.kotlin.argumentCaptor
|
import org.mockito.kotlin.argumentCaptor
|
||||||
import org.mockito.kotlin.doAnswer
|
import org.mockito.kotlin.doAnswer
|
||||||
import org.mockito.kotlin.doNothing
|
|
||||||
import org.mockito.kotlin.doReturn
|
import org.mockito.kotlin.doReturn
|
||||||
import org.mockito.kotlin.eq
|
import org.mockito.kotlin.eq
|
||||||
import org.mockito.kotlin.mock
|
import org.mockito.kotlin.mock
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.io.StringReader
|
import java.io.StringReader
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
import java.util.*
|
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.HttpServletRequest
|
||||||
import javax.servlet.http.HttpServletResponse
|
import javax.servlet.http.HttpServletResponse
|
||||||
|
|
||||||
|
|
||||||
|
// J2EE server basic mocking
|
||||||
|
|
||||||
object TestAPI {
|
object TestAPI {
|
||||||
|
|
||||||
fun Any?.toUnit() = Unit
|
fun Any?.toUnit() = Unit
|
||||||
|
|
||||||
private val apiServlet = ApiServlet()
|
private val apiServlet = ApiServlet()
|
||||||
|
|
||||||
private fun testRequest(reqMethod: String, uri: String, payload: Json? = null): Json {
|
private fun <T> testRequest(reqMethod: String, uri: String, payload: T? = null): Json {
|
||||||
|
|
||||||
WebappManager.properties["webapp.env"] = "test"
|
WebappManager.properties["webapp.env"] = "test"
|
||||||
|
|
||||||
@@ -48,7 +56,11 @@ object TestAPI {
|
|||||||
on { localName } doReturn "pairgoth"
|
on { localName } doReturn "pairgoth"
|
||||||
on { localPort } doReturn 80
|
on { localPort } doReturn 80
|
||||||
on { contextPath } doReturn ""
|
on { contextPath } doReturn ""
|
||||||
on { contentType } doReturn if (reqMethod == "GET") null else "application/json; charset=UTF-8"
|
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 { headerNames } doReturn Collections.enumeration(myHeaderNames)
|
||||||
on { getHeader(eq("Accept")) } doReturn "application/json"
|
on { getHeader(eq("Accept")) } doReturn "application/json"
|
||||||
}
|
}
|
||||||
@@ -72,12 +84,12 @@ object TestAPI {
|
|||||||
return Json.parse(buffer.toString()) ?: throw Error("no response payload")
|
return Json.parse(buffer.toString()) ?: throw Error("no response payload")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get(uri: String) = testRequest("GET", uri)
|
fun get(uri: String) = testRequest<Void>("GET", uri)
|
||||||
fun post(uri: String, payload: Json) = testRequest("POST", uri, payload)
|
fun <T> post(uri: String, payload: T) = testRequest("POST", uri, payload)
|
||||||
fun put(uri: String, payload: Json) = testRequest("PUT", uri, payload)
|
fun <T> put(uri: String, payload: T) = testRequest("PUT", uri, payload)
|
||||||
fun delete(uri: String, payload: Json) = testRequest("DELETE", uri, payload)
|
fun <T> delete(uri: String, payload: T) = testRequest("DELETE", uri, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun expectSuccess() {
|
// Get a list of resources
|
||||||
|
|
||||||
}
|
fun getTestResources(path: String) = File("${System.getProperty("user.dir")}/src/test/resources/$path").listFiles()
|
||||||
|
Reference in New Issue
Block a user