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.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: 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) 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""" private fun kindOf(type: String, styp: String): MsgKind = when (type) { "SCHD" -> MsgKind.Schd( when (styp) { "RESP" -> MsgKind.SchdSubtype.RESP "ADFT" -> MsgKind.SchdSubtype.ADFT else -> MsgKind.SchdSubtype.DNLD }, ) "FLOP" -> if (styp == "FDEL") MsgKind.Fdel else MsgKind.Flop(styp) else -> MsgKind.Unsupported("$type-$styp") } }