Various debugging; OpenGotha import draft

This commit is contained in:
Claude Brisson
2023-05-19 10:27:42 +02:00
parent d4b6b847f5
commit 8e7c48d7e9
11 changed files with 1184 additions and 37 deletions

View File

@@ -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)

View 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
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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")
} }

View File

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

View File

@@ -0,0 +1,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)

View File

@@ -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,11 +172,7 @@ class ApiServlet : HttpServlet() {
) )
// check content type // check content type
if (!isJson(mimeType)) throw ApiException( if (isJson(mimeType)) {
HttpServletResponse.SC_BAD_REQUEST,
"JSON content expected"
)
// put Json body as request attribute // put Json body as request attribute
try { try {
Json.parse(request.reader)?.let { payload: Json -> Json.parse(request.reader)?.let { payload: Json ->
@@ -185,6 +184,24 @@ class ApiServlet : HttpServlet() {
} catch (ioe: IOException) { } catch (ioe: IOException) {
throw ApiException(HttpServletResponse.SC_BAD_REQUEST, ioe) throw ApiException(HttpServletResponse.SC_BAD_REQUEST, ioe)
} }
} else if (isXml(mimeType)) {
// some API calls like opengotha import accept xml docs as body
// CB TODO - limit to those calls
try {
XmlUtils.parse(request.reader)?.let { payload: Element ->
request.setAttribute(ApiHandler.PAYLOAD_KEY, payload)
logger.info(blue("<< (xml document)"))
}
} catch(ioe: IOException) {
throw ApiException(HttpServletResponse.SC_BAD_REQUEST, ioe)
}
}
else throw ApiException(
HttpServletResponse.SC_BAD_REQUEST,
"JSON content expected"
)
} }
@Throws(ApiException::class) @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")
}
} }
} }

View 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())
}
}
}

View File

@@ -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 =====")
} }
} }

View File

@@ -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()