package com.gzzn.omms.msgexchange.codec import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.JsonMappingException import com.fasterxml.jackson.module.kotlin.registerKotlinModule 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 jakarta.inject.Singleton import javax.xml.stream.XMLInputFactory /** 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 { setProperty(XMLInputFactory.SUPPORT_DTD, false) setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false) }, ), ).apply { registerKotlinModule() // SIS explicitly permits fields unused by a subsystem; wire DTOs document that policy. configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) } 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")) } val msg = try { mapper.readValue(trimmed, SisMessageXml::class.java) } catch (e: JsonMappingException) { // META 层绑定失败=发送方违反必填契约(MALFORMED);体层(SCHD/FLOP)结构不匹配 // =wire 结构超出编解码器当前绑定能力(CODEC_ERROR:退避重试,修复后可重放,docs/implementation.md「状态与错误分类」)。 val first = e.path.firstOrNull()?.fieldName ?: "" if (first.equals("meta", ignoreCase = true)) { return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "xml-meta:${e.originalMessage.take(200)}")) } return DecodeResult.Err(DecodeFailure(ErrorClass.CODEC_ERROR, "xml-mapping:${e.originalMessage.take(200)}")) } catch (e: Exception) { return DecodeResult.Err( DecodeFailure(ErrorClass.MALFORMED, "xml-parse:${e.message?.take(200) ?: e.javaClass.simpleName}"), ) } val meta = msg.meta ?: return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "missing-meta")) val sndr = meta.sndr?.trim()?.takeIf { it.isNotEmpty() } ?: return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "missing-sndr")) val seqn = meta.seqn ?: 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?.trim()?.uppercase() ?: return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "missing-type")) 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 -> msg.schd?.let(SisWireMapper::scheduleBody) is MsgKind.Flop -> msg.flop?.let(SisWireMapper::flopPayload) MsgKind.Fdel -> msg.flop?.let(SisWireMapper::flopPayload) // 参考数据类别报文的正文结构随 13 类各异(ACM2-93 实装 ReferenceDataProcessor 时绑定) is MsgKind.RefData -> null is MsgKind.Unsupported -> null } return DecodeResult.Ok( DecodedMessage( meta = MetaFields(sndr = sndr, type = type, styp = styp, seqn = seqn, dttm = dttm), kind = kind, rawXml = trimmed, body = body, ), ) } override fun encodeRqrd(kind: String, rangeJson: String): String = """RQFD""" /** * 子类型分派必须是**白名单**:未知的 SCHD 子类型不能静默当成全量日计划(DNLD)—— * 那会让一条本该"暂不支持、等人工处置"的报文走最重的整包合并写入路径。 * 空 STYP 按 legacy 约定视为 DNLD(该约定待与 SIS 逐类对拍确认,见 Q3)。 * FLOP 同样按白名单:仅现行七类(`implementation.md`「动态运行事件」`US-05` AC1) * 走合并路径,其余 STYP 是 `Q3` 闭合目标,落 `Unsupported`(`FDEL` 单独成类,不参与白名单)。 * 静态参考数据类别码(`implementation.md`「静态参考数据」`SIS:3.1`~`3.14`)单列成 * [MsgKind.RefData]:它们必须走参考数据处理(US-13),不得混入"不支持"跳过。 */ private fun kindOf(type: String, styp: String): MsgKind = when (type) { "SCHD" -> when (styp) { "RESP" -> MsgKind.Schd(MsgKind.SchdSubtype.RESP) "ADFT" -> MsgKind.Schd(MsgKind.SchdSubtype.ADFT) "DNLD", "" -> MsgKind.Schd(MsgKind.SchdSubtype.DNLD) else -> MsgKind.Unsupported("SCHD-$styp") } "FLOP" -> when { styp == "FDEL" -> MsgKind.Fdel styp in FLOP_SUBTYPES -> MsgKind.Flop(styp) else -> MsgKind.Unsupported("FLOP-$styp") } in REF_DATA_TYPES -> MsgKind.RefData(type) else -> MsgKind.Unsupported("$type-$styp") } /** 现行 FLOP 白名单 7 类(`US-05` AC1);表内其余行是 `Q3` 闭合目标,不是现行白名单。 */ private val FLOP_SUBTYPES = setOf("ABTM", "DELY", "PSDT", "CKDT", "CLDT", "CHDT", "GTDT") private val REF_DATA_TYPES = setOf( "COUL", "ARPT", "AIRL", "AIRC", "REGN", "ORGN", "FLTL", "TLST", "GLST", "SLST", "CLST", "BLST", "CHLT", "RSTA", ) }