176 lines
7.3 KiB
Kotlin
176 lines
7.3 KiB
Kotlin
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",
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|