refactor(codec): map SIS XML through annotated DTOs

This commit is contained in:
windyboy
2026-09-09 19:48:45 +08:00
parent c2d4a76e3e
commit 3ed54d306e
7 changed files with 290 additions and 157 deletions
@@ -2,26 +2,18 @@ package com.gzzn.omms.msgexchange.codec
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule
import com.fasterxml.jackson.dataformat.xml.XmlFactory
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.gzzn.omms.msgexchange.domain.DecodedMessage
import com.gzzn.omms.msgexchange.domain.ErrorClass
import com.gzzn.omms.msgexchange.domain.MetaFields
import com.gzzn.omms.msgexchange.domain.MsgKind
import com.gzzn.omms.msgexchange.domain.flight.ScheduleRecord
import jakarta.inject.Singleton
import javax.xml.stream.XMLInputFactory
/**
* SIS XML 解码:jackson-dataformat-xml 直接映射到 [SisMessage] 数据类(不再手写 DOM)。
* XXE 防护:XMLInputFactory 禁 DTD/外部实体。
* 覆盖:SCHD DNLD/RESPFLTR 记录集,§5 入口)、SCHD ADFT(单记录 §2.1)、
* FLOP(单航班增量 §6.1)、FDEL(终止实例 §6.2)。
*/
/** XML codec for the SIS wire contract; XML DTO binding is kept separate from domain payloads. */
@Singleton
class JacksonXmlCodec : XmlCodec {
private val mapper: XmlMapper = XmlMapper(
XmlFactory(
XMLInputFactory.newInstance().apply {
@@ -31,6 +23,7 @@ class JacksonXmlCodec : XmlCodec {
),
).apply {
registerKotlinModule()
// SIS explicitly permits fields unused by a subsystem; wire DTOs document that policy.
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
}
@@ -40,7 +33,7 @@ class JacksonXmlCodec : XmlCodec {
return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "empty-or-non-xml"))
}
val msg = try {
mapper.readValue(trimmed, SisMessage::class.java)
mapper.readValue(trimmed, SisMessageXml::class.java)
} catch (e: Exception) {
return DecodeResult.Err(
DecodeFailure(ErrorClass.MALFORMED, "xml-parse:${e.message?.take(200) ?: e.javaClass.simpleName}"),
@@ -54,16 +47,16 @@ class JacksonXmlCodec : XmlCodec {
?: return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "missing-or-invalid-seqn"))
val dttm = meta.dttm
?: return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "missing-or-invalid-dttm"))
val type = meta.type?.uppercase()
val type = meta.type?.trim()?.uppercase()
?: return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "missing-type"))
val styp = meta.styp?.uppercase()
val styp = meta.styp?.trim()?.uppercase()
?: return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "missing-styp"))
val kind = kindOf(type, styp)
val body: Any? = when (kind) {
is MsgKind.Schd -> parseScheduleBody(msg)
is MsgKind.Flop -> parseFlightSection(msg.flop)
MsgKind.Fdel -> parseFlightSection(msg.fdel)
is MsgKind.Schd -> msg.schd?.let(SisWireMapper::scheduleBody)
is MsgKind.Flop -> msg.flop?.let(SisWireMapper::flopPayload)
MsgKind.Fdel -> msg.flop?.let(SisWireMapper::flopPayload)
is MsgKind.Unsupported -> null
}
@@ -88,93 +81,7 @@ class JacksonXmlCodec : XmlCodec {
else -> MsgKind.SchdSubtype.DNLD
},
)
"FDEL" -> MsgKind.Fdel
"FLOP" -> MsgKind.Flop(styp)
else -> MsgKind.Unsupported("$type-$styp") // §9 未支持类型,不猜测分派
}
/** SCHD DNLD/RESP/ADFTRECS 缺失/非法 → -1(处理层 §5.2 整包拒绝)。 */
private fun parseScheduleBody(msg: SisMessage): ScheduleBody? {
val section = msg.schd ?: return null
val records = section.fltr.mapNotNull { recordOf(it) }
return ScheduleBody(recsDeclared = section.recs ?: -1, records = records)
}
/** FLOP/FDEL:单航班段 Map → 载荷;无 FLID 返回 null(调用方按缺载荷处理)。 */
private fun parseFlightSection(section: Map<String, Any>?): FlopPayload? {
section ?: return null
val scalars = linkedMapOf<String, String>()
val collections = linkedMapOf<String, MutableList<Map<String, String>>>()
var flid: String? = null
section.forEach { (key, value) ->
val tag = key.uppercase()
when {
tag == "FLID" -> flid = value.toString().trim()
tag in COLLECTION_TAGS -> value.toCollectionItems()?.let { items ->
collections.getOrPut(tag) { mutableListOf() }.addAll(items)
}
value is String -> scalars[tag] = value.trim()
}
}
return flid?.takeIf { it.isNotEmpty() }?.let {
FlopPayload(flid = it, scalars = scalars, collections = collections.mapValues { m -> m.value.toList() })
}
}
/** FLTR Map → ScheduleRecord:标量/集合分离,集合项含源序号属性。 */
private fun recordOf(fltr: Map<String, Any>): ScheduleRecord? {
val scalars = linkedMapOf<String, String>()
val collections = linkedMapOf<String, MutableList<Map<String, String>>>()
var flid: String? = null
var seqn = 0L
fltr.forEach { (key, value) ->
val tag = key.uppercase()
when {
tag == "FLID" -> flid = value.toString().trim()
tag == "SEQN" -> seqn = (value as? Number)?.toLong() ?: value.toString().trim().toLongOrNull() ?: 0L
tag in COLLECTION_TAGS -> value.toCollectionItems()?.let { items ->
collections.getOrPut(tag) { mutableListOf() }.addAll(items)
}
value is String -> scalars[tag] = value.trim()
}
}
return flid?.takeIf { it.isNotEmpty() }?.let {
ScheduleRecord(flid = it, scalars = scalars, collections = collections.mapValues { m -> m.value.toList() }, seqn = seqn)
}
}
/**
* 集合值归一:重复元素 → List<Map>(属性与子元素展平);单元素 → 单项列表;
* 混合文本(如 DELY 备注文本)落到 REMC 键(XmlMapper 绑定的已知弱项,尽力保留)。
*/
private fun Any?.toCollectionItems(): List<Map<String, String>>? = when (this) {
is List<*> -> mapNotNull { it?.toCollectionItem() }
is Map<*, *> -> listOfNotNull(toCollectionItem(this))
is String -> listOf(mapOf("REMC" to trim()))
else -> null
}
private fun toCollectionItem(map: Map<*, *>): Map<String, String> {
val out = linkedMapOf<String, String>()
map.forEach { (k, v) ->
val key = k.toString().uppercase()
when (v) {
is String -> out[key] = v.trim()
is Number -> out[key] = v.toString()
}
}
return out
}
private fun Any?.toCollectionItem(): Map<String, String>? = when (this) {
is Map<*, *> -> toCollectionItem(this)
is String -> mapOf("REMC" to trim())
else -> null
}
companion object {
val COLLECTION_TAGS: Set<String> = setOf(
"GTDT", "CKDT", "CLDT", "PSDT", "CHDT", "DELY", "ABTM", "CHOT", "ROUT", "ERUT",
)
"FLOP" -> if (styp == "FDEL") MsgKind.Fdel else MsgKind.Flop(styp)
else -> MsgKind.Unsupported("$type-$styp")
}
}