From 039e841af85c7a52223351696f8b40a8203f0e4f Mon Sep 17 00:00:00 2001 From: lumber1000 <45400511+lumber1000@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:04:47 +0400 Subject: [PATCH] Date/Time/Boolean values in parsed messages in "string mode" fixed --- .../exactpro/th2/codec/fixng/FixNgCodec.kt | 58 ++++++++++++++----- .../th2/codec/fixng/FixNgCodecTest.kt | 45 ++++++-------- 2 files changed, 59 insertions(+), 44 deletions(-) diff --git a/src/main/kotlin/com/exactpro/th2/codec/fixng/FixNgCodec.kt b/src/main/kotlin/com/exactpro/th2/codec/fixng/FixNgCodec.kt index 6a16557..581085a 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/fixng/FixNgCodec.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/fixng/FixNgCodec.kt @@ -81,9 +81,9 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) val messageFields = message.body @Suppress("UNCHECKED_CAST") - val headerFields = messageFields[HEADER] as? Map ?: mapOf() + val headerFields = messageFields[HEADER] as? Map ?: mapOf() @Suppress("UNCHECKED_CAST") - val trailerFields = messageFields[TRAILER] as? Map ?: mapOf() + val trailerFields = messageFields[TRAILER] as? Map ?: mapOf() val body = Unpooled.buffer(1024) val prefix = Unpooled.buffer(32) @@ -281,7 +281,15 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) handleError(isDirty, context, "Wrong date/time value in ${primitiveType.name} field '$name'. Value: $value.", value) } - return if (isDecodeToStrings) value else decodedValue + return if (isDecodeToStrings) { + if (primitiveType == java.time.LocalDateTime::class.java || primitiveType == java.time.LocalDate::class.java || primitiveType == java.time.LocalTime::class.java || primitiveType == java.lang.Boolean::class.java) { + decodedValue.toString() + } else { + value + } + } else { + decodedValue + } } private fun Group.decode(source: ByteBuf, count: Int, isDirty: Boolean, context: IReportingContext): List> = ArrayList>().also { list -> @@ -318,22 +326,40 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) private fun encodeField(field: Field, value: Any, target: ByteBuf, isDirty: Boolean, dictionaryFields: Map, context: IReportingContext) { when { field is Primitive -> { - if (!isCompatibleType(value.javaClass, field.primitiveType)) { - if (value is String) { - field.decode(value, isDirty, context) // validate if String value could be parsed to required type - target.writeField(field.tag, value, charset) - return - } else { - handleError(isDirty, context, "Wrong type value in field ${field.name}. Actual: ${value.javaClass} (value: $value). Expected ${field.primitiveType}") + val valueToEncode = when { + isCompatibleType(value.javaClass, field.primitiveType) -> value + value is String -> { + try { + when (field.primitiveType) { + LocalDateTime::class.java -> LocalDateTime.parse(value) + LocalDate::class.java -> LocalDate.parse(value) + LocalTime::class.java -> LocalTime.parse(value) + java.lang.Boolean::class.java -> when { + value.equals("true", true) -> true + value.equals("false", true) -> false + else -> handleError(isDirty, context, "Wrong boolean value in ${field.primitiveType.name} field '$field.name'. Value: $value.", value) + } + else -> { + // we reuse decode() method for the types that have the same string representation + // of values in FIX protocol and in TH2 transport protocol + field.decode(value, isDirty, context) // validate if String value could be parsed to required type + target.writeField(field.tag, value, charset) + return + } + } + } catch (e: DateTimeParseException) { + handleError(isDirty, context, "Wrong date/time value in ${field.primitiveType.name} field '$field.name'. Value: $value.", value) + } } + else -> handleError(isDirty, context, "Wrong type value in field ${field.name}. Actual: ${value.javaClass} (value: $value). Expected ${field.primitiveType}") } - val stringValue = when (value) { - is java.lang.Boolean -> if (value.booleanValue()) "Y" else "N" - is LocalDateTime -> value.format(dateTimeFormatter) - is LocalDate -> value.format(dateFormatter) - is LocalTime -> value.format(timeFormatter) - else -> value.toString() + val stringValue = when (valueToEncode) { + is LocalDateTime -> valueToEncode.format(dateTimeFormatter) + is LocalDate -> valueToEncode.format(dateFormatter) + is LocalTime -> valueToEncode.format(timeFormatter) + is java.lang.Boolean -> if (valueToEncode.booleanValue()) "Y" else "N" + else -> valueToEncode.toString() } when { diff --git a/src/test/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecTest.kt b/src/test/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecTest.kt index d69346c..382613a 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecTest.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecTest.kt @@ -34,12 +34,7 @@ import org.junit.jupiter.api.Test import java.math.BigDecimal import java.nio.charset.StandardCharsets import java.time.Instant -import java.time.LocalDate import java.time.LocalDateTime -import java.time.LocalTime -import java.time.format.DateTimeFormatter -import java.time.format.DateTimeFormatterBuilder -import java.time.temporal.ChronoField class FixNgCodecTest { private val dictionary: IDictionaryStructure = FixNgCodecTest::class.java.classLoader @@ -66,11 +61,14 @@ class FixNgCodecTest { @Test fun `simple encode`() = encodeTest(MSG_CORRECT) + @Test + fun `simple encode from string values`() = encodeTest(MSG_CORRECT, encodeFromStringValues = true) + @Test fun `simple decode`() = decodeTest(MSG_CORRECT) @Test - fun `simple decode to string values`() = decodeTest(MSG_CORRECT, stringValues = true) + fun `simple decode to string values`() = decodeTest(MSG_CORRECT, decodeToStringValues = true) @Test fun `simple decode with no body`() = decodeTest(MSG_CORRECT_WITHOUT_BODY, expectedMessage = expectedMessageWithoutBody) @@ -123,7 +121,7 @@ class FixNgCodecTest { @Test fun `encode with required delimiter field in group removed in first entry`() { @Suppress("UNCHECKED_CAST") - ((parsedBody["TradingParty"] as Map)["NoPartyIDs"] as List>)[0].remove("PartyID") + ((parsedBody["TradingParty"] as Map)["NoPartyIDs"] as List>)[0].remove("PartyID") encodeTest(MSG_DELIMITER_FIELD_IN_GROUP_REMOVED_IN_FIRST_ENTRY, "Required field missing. Field name: PartyID.") } @@ -137,7 +135,7 @@ class FixNgCodecTest { @Test fun `encode with required delimiter field in group removed in second entry`() { @Suppress("UNCHECKED_CAST") - ((parsedBody["TradingParty"] as Map)["NoPartyIDs"] as List>)[1].remove("PartyID") + ((parsedBody["TradingParty"] as Map)["NoPartyIDs"] as List>)[1].remove("PartyID") encodeTest(MSG_DELIMITER_FIELD_IN_GROUP_REMOVED_IN_SECOND_ENTRY, "Required field missing. Field name: PartyID.") } @@ -237,8 +235,15 @@ class FixNgCodecTest { private fun encodeTest( expectedRawMessage: String, - expectedWarning: String? = null + expectedWarning: String? = null, + encodeFromStringValues: Boolean = false ) { + if (encodeFromStringValues) { + @Suppress("UNCHECKED_CAST") + val stringBody = convertValuesToString(parsedBody) as Map + parsedBody.putAll(stringBody) + } + val encoded = codec.encode(MessageGroup(listOf(parsedMessage)), reportingContext) val body = encoded.messages.single().body as CompositeByteBuf val fixMsg = body.toString(StandardCharsets.US_ASCII) @@ -255,7 +260,7 @@ class FixNgCodecTest { expectedErrorText: String? = null, expectedMessage: ParsedMessage = expectedParsedMessage, dirtyMode: Boolean = true, - stringValues: Boolean = false + decodeToStringValues: Boolean = false ) { val expectedBody = expectedMessage.body val rawMessage = RawMessage( @@ -266,7 +271,7 @@ class FixNgCodecTest { ) val decodedGroup = try { - val codec = if (stringValues) FixNgCodec(dictionary, FixNgCodecSettings(dictionary = "")) else this.codec + val codec = if (decodeToStringValues) FixNgCodec(dictionary, FixNgCodecSettings(dictionary = "")) else this.codec codec.decode(MessageGroup(listOf(rawMessage)), reportingContext) } catch (e: IllegalStateException) { if (dirtyMode) { @@ -281,7 +286,7 @@ class FixNgCodecTest { // we don't validate `CheckSum` and `BodyLength` in incorrect messages val fieldsToIgnore = if (expectedErrorText == null) emptyArray() else arrayOf("trailer.CheckSum", "header.BodyLength") - val expected = if (stringValues) convertValuesToString(expectedBody) else expectedBody + val expected = if (decodeToStringValues) convertValuesToString(expectedBody) else expectedBody assertThat(parsedMessage.body) .usingRecursiveComparison() @@ -298,10 +303,6 @@ class FixNgCodecTest { private fun convertValuesToString(value: Any?): Any = when (value) { is Map<*, *> -> value.mapValues { convertValuesToString(it.value) } is List<*> -> value.map(::convertValuesToString) - is java.lang.Boolean -> if (value.booleanValue()) "Y" else "N" - is LocalDateTime -> value.format(dateTimeFormatter) - is LocalDate -> value.format(dateFormatter) - is LocalTime -> value.format(timeFormatter) else -> value.toString() } @@ -456,17 +457,5 @@ class FixNgCodecTest { private const val MSG_NON_PRINTABLE = "8=FIXT.1.1\u00019=303\u000135=8\u000149=SENDER\u000156=RECEIVER\u000134=10947\u000152=20230419-10:36:07.415088\u000117=495504662\u000111=zSuNbrBIZyVljs\u000141=zSuNbrBIZyVljs\u000137=49415882\u0001150=0\u000139=0\u0001151=500\u000114=500\u000148=NWDR\u000122=8\u0001453=2\u0001448=NGALL1FX01\u0001447=D\u0001452=76\u0001448=0\u0001447=P\u0001452=3\u00011=test\taccount\u000140=A\u000159=0\u000154=B\u000155=ABC\u000138=500\u000144=1000\u000147=500\u000160=20180205-10:38:08.000008\u000110=171\u0001" private const val MSG_REQUIRED_HEADER_REMOVED = "8=FIXT.1.1\u00019=236\u000135=8\u000117=495504662\u000111=zSuNbrBIZyVljs\u000141=zSuNbrBIZyVljs\u000137=49415882\u0001150=0\u000139=0\u0001151=500\u000114=500\u000148=NWDR\u000122=8\u0001453=2\u0001448=NGALL1FX01\u0001447=D\u0001452=76\u0001448=0\u0001447=P\u0001452=3\u00011=test\u000140=A\u000159=0\u000154=B\u000155=ABC\u000138=500\u000144=1000\u000147=500\u000160=20180205-10:38:08.000008\u000110=050\u0001" private const val MSG_TAG_OUT_OF_ORDER = "8=FIXT.1.1\u00019=295\u000135=8\u000149=SENDER\u000156=RECEIVER\u000134=10947\u000152=20230419-10:36:07.415088\u000117=495504662\u000111=zSuNbrBIZyVljs\u000141=zSuNbrBIZyVljs\u000137=49415882\u0001150=0\u000139=0\u0001151=500\u000114=500\u000148=NWDR\u000122=8\u0001453=2\u0001448=NGALL1FX01\u0001447=D\u0001452=76\u0001448=0\u0001447=P\u0001452=3\u00011=test\u000140=A\u000159=0\u000154=B\u000155=ABC\u000138=500\u000144=1000\u000147=500\u000160=20180205-10:38:08.000008\u000110=000\u0001999=500\u0001" - - private val dateTimeFormatter = DateTimeFormatterBuilder() - .appendPattern("yyyyMMdd-HH:mm:ss") - .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true) - .toFormatter() - - private val dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd") - - private val timeFormatter = DateTimeFormatterBuilder() - .appendPattern("HH:mm:ss") - .appendFraction(ChronoField.MILLI_OF_SECOND, 0, 9, true) - .toFormatter() } } \ No newline at end of file