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

88 lines
3.7 KiB
Kotlin

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 =
"""<?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
"ADFT" -> MsgKind.SchdSubtype.ADFT
else -> MsgKind.SchdSubtype.DNLD
},
)
"FLOP" -> if (styp == "FDEL") MsgKind.Fdel else MsgKind.Flop(styp)
else -> MsgKind.Unsupported("$type-$styp")
}
}