Files
msgexchange-v2/src/main/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodec.kt
T

176 lines
7.3 KiB
Kotlin
Raw Normal View History

package com.gzzn.omms.msgexchange.codec
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 jakarta.inject.Singleton
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.xml.sax.InputSource
import java.io.StringReader
import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory
/**
* SIS XML 解码(XXE 防护;META + TYPE 分派 + FLOP 集合解析)。
* 阶段 2 先覆盖 FLOP-GTDT 纵向链路所需字段;其余 STYP 仅完成 META/FLID 提取。
*/
@Singleton
class JacksonXmlCodec : XmlCodec {
private val builderFactory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance().apply {
isNamespaceAware = false
isExpandEntityReferences = false
setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
setFeature("http://xml.org/sax/features/external-general-entities", false)
setFeature("http://xml.org/sax/features/external-parameter-entities", false)
try {
setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "")
setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "")
} catch (_: IllegalArgumentException) {
// JDK 实现差异:忽略不支持的属性
}
}
override fun decode(rawXml: String): DecodeResult {
val trimmed = rawXml.trim()
if (trimmed.isEmpty() || !trimmed.startsWith("<")) {
return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "empty-or-non-xml"))
}
return try {
val doc = builderFactory.newDocumentBuilder().parse(InputSource(StringReader(trimmed)))
val root = doc.documentElement ?: return DecodeResult.Err(
DecodeFailure(ErrorClass.MALFORMED, "missing-root"),
)
if (!root.tagName.equals("MSG", ignoreCase = true)) {
return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "root-not-msg:${root.tagName}"))
}
val metaEl = firstChildElement(root, "META")
?: return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "missing-meta"))
val sndr = childText(metaEl, "SNDR")
?: return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "missing-sndr"))
val seqn = childText(metaEl, "SEQN")?.toLongOrNull()
?: return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "missing-or-invalid-seqn"))
val dttm = childText(metaEl, "DTTM")?.toLongOrNull()
?: return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "missing-or-invalid-dttm"))
val type = childText(metaEl, "TYPE")?.uppercase()
?: return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "missing-type"))
val styp = childText(metaEl, "STYP")?.uppercase()
?: return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "missing-styp"))
val kind = kindOf(type, styp)
val body = when (kind) {
is MsgKind.Flop -> parseFlopBody(root, type)
is MsgKind.Schd -> null
}
DecodeResult.Ok(
DecodedMessage(
meta = MetaFields(sndr = sndr, type = type, styp = styp, seqn = seqn, dttm = dttm),
kind = kind,
rawXml = trimmed,
body = body,
),
)
} catch (e: NumberFormatException) {
DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "invalid-number:${e.message}"))
} catch (e: Exception) {
DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "xml-parse:${e.message ?: e.javaClass.simpleName}"))
}
}
override fun encodeRqrd(kind: String, rangeJson: String): String =
"""<?xml version="1.0" encoding="UTF-8"?><MSG><META><TYPE>RQFD</TYPE></META></MSG>"""
private fun kindOf(type: String, styp: String): MsgKind = when (type) {
"SCHD" -> MsgKind.Schd(
when (styp) {
"RESP" -> MsgKind.SchdSubtype.RESP
"DNLD" -> MsgKind.SchdSubtype.DNLD
"ADFT" -> MsgKind.SchdSubtype.ADFT
else -> MsgKind.SchdSubtype.DNLD
},
)
"FLOP" -> MsgKind.Flop(styp)
else -> MsgKind.Flop(styp)
}
private fun parseFlopBody(root: Element, type: String): FlopPayload? {
val section = firstChildElement(root, type) ?: return null
val scalars = linkedMapOf<String, String>()
val collections = linkedMapOf<String, MutableList<Map<String, String>>>()
section.childNodes.let { nodes ->
for (i in 0 until nodes.length) {
val node = nodes.item(i)
if (node.nodeType != Node.ELEMENT_NODE) continue
val el = node as Element
val tag = el.tagName.uppercase()
when {
tag in COLLECTION_TAGS -> {
collections.getOrPut(tag) { mutableListOf() }.add(parseCollectionItem(el))
}
tag == "FLID" -> scalars["FLID"] = el.textContent.trim()
hasElementChildren(el) -> Unit
else -> scalars[tag] = el.textContent.trim()
}
}
}
val flid = scalars.remove("FLID") ?: return null
return FlopPayload(flid = flid, scalars = scalars, collections = collections.mapValues { it.value.toList() })
}
private fun parseCollectionItem(el: Element): Map<String, String> {
val item = linkedMapOf<String, String>()
if (el.hasAttributes()) {
for (i in 0 until el.attributes.length) {
val attr = el.attributes.item(i)
item[attr.nodeName.uppercase()] = attr.nodeValue.trim()
}
}
el.childNodes.let { nodes ->
for (i in 0 until nodes.length) {
val node = nodes.item(i)
if (node.nodeType != Node.ELEMENT_NODE) continue
val child = node as Element
item[child.tagName.uppercase()] = child.textContent.trim()
}
}
return item
}
private fun firstChildElement(parent: Element, tag: String): Element? {
parent.childNodes.let { nodes ->
for (i in 0 until nodes.length) {
val node = nodes.item(i)
if (node.nodeType == Node.ELEMENT_NODE && (node as Element).tagName.equals(tag, ignoreCase = true)) {
return node
}
}
}
return null
}
private fun childText(parent: Element, tag: String): String? =
firstChildElement(parent, tag)?.textContent?.trim()?.takeIf { it.isNotEmpty() }
private fun hasElementChildren(el: Element): Boolean {
el.childNodes.let { nodes ->
for (i in 0 until nodes.length) {
if (nodes.item(i).nodeType == Node.ELEMENT_NODE) return true
}
}
return false
}
companion object {
val COLLECTION_TAGS: Set<String> = setOf(
"GTDT", "CKDT", "CLDT", "PSDT", "CHDT", "DELY", "ABTM", "CHOT", "ROUT", "ERUT",
)
}
}