Various debugging; OpenGotha import draft
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
package org.jeudego.pairgoth.api
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.PAYLOAD_KEY
|
||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||
import org.jeudego.pairgoth.ext.OpenGotha
|
||||
import org.jeudego.pairgoth.model.Tournament
|
||||
import org.jeudego.pairgoth.model.fromJson
|
||||
import org.jeudego.pairgoth.model.toJson
|
||||
import org.jeudego.pairgoth.store.Store
|
||||
import org.w3c.dom.Element
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
object TournamentHandler: PairgothApiHandler {
|
||||
@@ -18,10 +21,11 @@ object TournamentHandler: PairgothApiHandler {
|
||||
}
|
||||
|
||||
override fun post(request: HttpServletRequest): Json {
|
||||
val payload = getObjectPayload(request)
|
||||
|
||||
// tournament parsing
|
||||
val tournament = Tournament.fromJson(payload)
|
||||
val tournament = when (val payload = request.getAttribute(PAYLOAD_KEY)) {
|
||||
is Json.Object -> Tournament.fromJson(getObjectPayload(request))
|
||||
is Element -> OpenGotha.import(payload)
|
||||
else -> badRequest("missing or invalid payload")
|
||||
}
|
||||
|
||||
Store.addTournament(tournament)
|
||||
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,
|
||||
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
|
||||
|
@@ -10,6 +10,7 @@ import kotlin.math.roundToInt
|
||||
// Pairable
|
||||
|
||||
sealed class Pairable(val id: Int, val name: String, open val rating: Int, open val rank: Int) {
|
||||
companion object {}
|
||||
abstract fun toJson(): Json.Object
|
||||
val skip = mutableSetOf<Int>() // skipped rounds
|
||||
}
|
||||
@@ -29,10 +30,10 @@ fun Pairable.displayRank(): String = when {
|
||||
|
||||
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 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()) {
|
||||
"k" -> -num
|
||||
"d" -> num - 1
|
||||
|
@@ -30,7 +30,7 @@ class SwissSolver(history: List<Game>, pairables: List<Pairable>, val method: Sw
|
||||
SPLIT_AND_FOLD ->
|
||||
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
|
||||
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
|
||||
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.PlayerHandler
|
||||
import org.jeudego.pairgoth.api.TournamentHandler
|
||||
import org.jeudego.pairgoth.util.Colorizer.blue
|
||||
import org.jeudego.pairgoth.util.Colorizer.green
|
||||
import org.jeudego.pairgoth.util.Colorizer.red
|
||||
import org.jeudego.pairgoth.util.XmlUtils
|
||||
import org.jeudego.pairgoth.util.parse
|
||||
import org.jeudego.pairgoth.util.toString
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.w3c.dom.Element
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.ReadWriteLock
|
||||
@@ -60,8 +63,8 @@ class ApiServlet : HttpServlet() {
|
||||
if ("dev" == WebappManager.getProperty("webapp.env")) {
|
||||
response.addHeader("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
validateContentType(request)
|
||||
validateAccept(request);
|
||||
validateContentType(request)
|
||||
|
||||
// parse request URI
|
||||
|
||||
@@ -169,22 +172,36 @@ class ApiServlet : HttpServlet() {
|
||||
)
|
||||
|
||||
// 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,
|
||||
"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)
|
||||
@@ -241,9 +258,7 @@ class ApiServlet : HttpServlet() {
|
||||
private 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")
|
||||
}
|
||||
private fun isJson(mimeType: String) = "text/json" == mimeType || "application/json" == mimeType || mimeType.endsWith("+json")
|
||||
private fun isXml(mimeType: String) = "text/xml" == mimeType || "application/xml" == mimeType || mimeType.endsWith("+xml")
|
||||
}
|
||||
}
|
||||
|
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.TestInfo
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
|
||||
abstract class TestBase {
|
||||
companion object {
|
||||
val logger = LoggerFactory.getLogger("test")
|
||||
private var testClassName: String? = null
|
||||
|
||||
@BeforeAll
|
||||
@JvmStatic
|
||||
fun prepare() {
|
||||
testClassName = this::class.simpleName
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun before(testInfo: TestInfo) {
|
||||
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.mockito.kotlin.argumentCaptor
|
||||
import org.mockito.kotlin.doAnswer
|
||||
import org.mockito.kotlin.doNothing
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.mock
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringReader
|
||||
import java.io.StringWriter
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipException
|
||||
import java.util.zip.ZipFile
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
|
||||
// J2EE server basic mocking
|
||||
|
||||
object TestAPI {
|
||||
|
||||
fun Any?.toUnit() = Unit
|
||||
|
||||
private val apiServlet = ApiServlet()
|
||||
|
||||
private 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"
|
||||
|
||||
@@ -48,7 +56,11 @@ object TestAPI {
|
||||
on { localName } doReturn "pairgoth"
|
||||
on { localPort } doReturn 80
|
||||
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 { getHeader(eq("Accept")) } doReturn "application/json"
|
||||
}
|
||||
@@ -72,12 +84,12 @@ object TestAPI {
|
||||
return Json.parse(buffer.toString()) ?: throw Error("no response payload")
|
||||
}
|
||||
|
||||
fun get(uri: String) = testRequest("GET", uri)
|
||||
fun post(uri: String, payload: Json) = testRequest("POST", uri, payload)
|
||||
fun put(uri: String, payload: Json) = testRequest("PUT", uri, payload)
|
||||
fun delete(uri: String, payload: Json) = testRequest("DELETE", uri, payload)
|
||||
fun get(uri: String) = testRequest<Void>("GET", uri)
|
||||
fun <T> post(uri: String, payload: T) = testRequest("POST", uri, payload)
|
||||
fun <T> put(uri: String, payload: T) = testRequest("PUT", 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