diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..bf685f0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 +updates: + - package-ecosystem: gradle + directory: / + reviewers: + - Nikita-Smirnov-Exactpro + - OptimumCode + labels: + - dependencies + schedule: + interval: daily + - package-ecosystem: github-actions + directory: / + reviewers: + - Nikita-Smirnov-Exactpro + - OptimumCode + labels: + - dependencies + schedule: + interval: daily \ No newline at end of file diff --git a/.github/workflows/ci-unwelcome-words.yml b/.github/workflows/ci-unwelcome-words.yml index 39d4010..8d4afbb 100644 --- a/.github/workflows/ci-unwelcome-words.yml +++ b/.github/workflows/ci-unwelcome-words.yml @@ -5,6 +5,7 @@ on: jobs: test: + if: github.actor != 'dependabot[bot]' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml new file mode 100644 index 0000000..613d18d --- /dev/null +++ b/.github/workflows/scan.yml @@ -0,0 +1,12 @@ +name: Scan licenses and vulnerabilities in java project + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 1' + +jobs: + build: + uses: th2-net/.github/.github/workflows/compound-java-scan.yml@main + secrets: + nvd-api-key: ${{ secrets.NVD_APIKEY }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 432f34f..13b1fc4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM gradle:8.7-jdk11 AS build +FROM gradle:8.11.1-jdk11 AS build ARG release_version COPY ./ . RUN gradle --no-daemon clean build dockerPrepare -Prelease_version=${release_version} diff --git a/README.md b/README.md index 33815b0..348e836 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# th2-codec-fix-ng 0.1.0 +# th2-codec-fix-ng 0.1.1 This codec can be used in dirty mode for decoding and encoding messages via the FIX protocol. @@ -14,7 +14,8 @@ Configuration example. ```yaml beginString: FIXT.1.1 dictionary: fix_dictionary.xml -charset: US_ASCII +charset: US-ASCII +decodeDelimiter: \u0001 dirtyMode: false decodeValuesToStrings: true decodeComponentsToNestedMaps: true @@ -27,7 +28,10 @@ default value: `FIXT.1.1`. Value to put into the `BeginString` field (tag: 8) wh required value. XML file containing the FIX dictionary. #### charset -default value: `US_ASCII`. Charset for reading and writing FIX fields. +default value: `US-ASCII`. Charset for reading and writing FIX fields. + +#### decodeDelimiter +default value: `\u0001`. Delimiter character from `US-ASCII` charset. #### dirtyMode default value: `false`. If `true`, processes all messages in dirty mode (generates warnings on invalid messages and continues processing). If `false`, only messages that contain the `encode-mode: dirty` property will be processed in dirty mode. @@ -42,12 +46,17 @@ default value: `true`. If `true`, decodes `components` to nested maps instead of Component benchmark results available [here](docs/benchmarks/jmh-benchmark.md). ## Release notes + +### 0.1.1 + + `decodeDelimiter` setting option added. + + Updated th2 gradle plugin `0.1.6` (th2-bom: `4.9.0`) + ### 0.1.0 + Dirty mode added. + `dirtyMode` setting option added. + `decodeValuesToStrings` setting option added. + JMH benchmarks added - + Migrate to th2 gradle plugin `0.1.2` (th2-bom:4.7.0) + + Migrate to th2 gradle plugin `0.1.2` (th2-bom: `4.7.0`) + Updated th2-common: `5.11.0-dev` + Updated th2-codec: `5.5.0-dev` + Updated sailfish: `3.3.241` diff --git a/build.gradle b/build.gradle index 57b3558..07c9bbe 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id "application" - id "com.exactpro.th2.gradle.component" version "0.1.2" + id "com.exactpro.th2.gradle.component" version "0.1.6" id "org.jetbrains.kotlin.jvm" version "$kotlin_version" id "org.jetbrains.kotlin.kapt" version "$kotlin_version" id "me.champeau.jmh" version "0.7.2" @@ -95,7 +95,7 @@ dependencies { jmh "org.openjdk.jmh:jmh-generator-annprocess:$jmhVersion" jmh "org.openjdk.jmh:jmh-generator-bytecode:$jmhVersion" - testImplementation "org.junit.jupiter:junit-jupiter:5.11.0" + testImplementation "org.junit.jupiter:junit-jupiter:5.11.3" testImplementation "org.jetbrains.kotlin:kotlin-test-junit5" testImplementation "org.assertj:assertj-core:3.26.3" } diff --git a/gradle.properties b/gradle.properties index 541b1e1..2ca9ee4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ kotlin.code.style=official kotlin_version=1.8.22 -release_version=0.1.0 +release_version=0.1.1 vcs_url=https://github.com/th2-net/th2-codec-fix-ng \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 093df1d..8fc91c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStorePath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/gradlew b/gradlew index 2fe81a7..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,78 +17,111 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -97,87 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 9618d8d..25da30d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,10 +25,14 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @@ -37,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -51,48 +55,36 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/src/main/kotlin/com/exactpro/th2/codec/fixng/ByteBufUtil.kt b/src/main/kotlin/com/exactpro/th2/codec/fixng/ByteBufUtil.kt index 8ecfe7f..98c9b18 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/fixng/ByteBufUtil.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/fixng/ByteBufUtil.kt @@ -20,7 +20,6 @@ import io.netty.buffer.ByteBuf import java.nio.charset.Charset private const val SEP_BYTE = '='.code.toByte() -private const val SOH_BYTE = 1.toByte() private const val DIGIT_0 = '0'.code.toByte() private const val DIGIT_9 = '9'.code.toByte() @@ -73,45 +72,48 @@ fun ByteBuf.writeTag(tag: Int): ByteBuf { return printInt(tag).writeByte(SEP_BYTE.toInt()) } -fun ByteBuf.readValue(charset: Charset, isDirty: Boolean): String { +fun ByteBuf.readValue(delimiter: Byte, charset: Charset, isDirty: Boolean): String { val offset = readerIndex() - val length = bytesBefore(SOH_BYTE) + val length = bytesBefore(delimiter) check(isDirty || length > 0) { "No valid value at offset: $offset" } readerIndex(offset + length + 1) return toString(offset, length, charset) } -fun ByteBuf.writeValue(value: String, charset: Charset): ByteBuf = apply { +fun ByteBuf.writeValue(value: String, delimiter: Byte, charset: Charset): ByteBuf = apply { writeCharSequence(value, charset) - writeByte(SOH_BYTE.toInt()) + writeByte(delimiter.toInt()) } inline fun ByteBuf.forEachField( + delimiter: Byte, charset: Charset, isDirty: Boolean, action: (tag: Int, value: String) -> Boolean, ) { while (isReadable) { val offset = readerIndex() - if (action(readTag(), readValue(charset, isDirty))) continue + if (action(readTag(), readValue(delimiter, charset, isDirty))) continue readerIndex(offset) break } } -inline fun ByteBuf.readField(tag: Int, charset: Charset, isDirty: Boolean, message: (Int) -> String): String = readTag().let { +inline fun ByteBuf.readField(tag: Int, delimiter: Byte, charset: Charset, isDirty: Boolean, message: (Int) -> String): String = readTag().let { check(it == tag) { message(it) } - readValue(charset, isDirty) + readValue(delimiter, charset, isDirty) } -fun ByteBuf.writeField(tag: Int, value: String, charset: Charset): ByteBuf = writeTag(tag).writeValue(value, charset) +fun ByteBuf.writeField(tag: Int, value: String, delimiter: Byte, charset: Charset): ByteBuf = + writeTag(tag).writeValue(value, delimiter, charset) -fun ByteBuf.writeField(tag: Int, value: Any?, charset: Charset): ByteBuf = writeField(tag, value.toString(), charset) +fun ByteBuf.writeField(tag: Int, value: Any?, delimiter: Byte, charset: Charset): ByteBuf = + writeField(tag, value.toString(), delimiter, charset) -fun ByteBuf.writeChecksum() { +fun ByteBuf.writeChecksum(delimiter: Byte) { val index = readerIndex() var checksum = 0 while (isReadable) checksum += readByte() readerIndex(index) - writeTag(10).printInt(checksum % 256, 3).writeByte(SOH_BYTE.toInt()) + writeTag(10).printInt(checksum % 256, 3).writeByte(delimiter.toInt()) } \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/codec/fixng/CharUtil.kt b/src/main/kotlin/com/exactpro/th2/codec/fixng/CharUtil.kt new file mode 100644 index 0000000..cfd706a --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/codec/fixng/CharUtil.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2024 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.codec.fixng + +fun Char.isPureAscii(): Boolean = Charsets.US_ASCII.newEncoder().canEncode(this) \ No newline at end of file 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 bb044be..e1f04cb 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/fixng/FixNgCodec.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/fixng/FixNgCodec.kt @@ -48,6 +48,9 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) private val isDirtyMode = settings.dirtyMode private val isDecodeToStrings = settings.decodeValuesToStrings private val isDecodeComponentsToNestedMaps = settings.decodeComponentsToNestedMaps + private val decodeDelimiter: Byte = settings.decodeDelimiter.also { + check(it.isPureAscii()) { "Tag delimiter '$it' isn't part of ${Charsets.US_ASCII} charset" } + }.code.toByte() private val fieldsEncode = convertToFieldsByName(dictionary.fields, true, emptyList(), true) private val fieldsDecode = convertToFieldsByTag(dictionary.fields, emptyList()) @@ -89,16 +92,16 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) val body = Unpooled.buffer(1024) val prefix = Unpooled.buffer(32) - prefix.writeField(TAG_BEGIN_STRING, beginString, charset) - body.writeField(TAG_MSG_TYPE, messageDef.type, charset) + prefix.writeField(TAG_BEGIN_STRING, beginString, SOH_BYTE, charset) + body.writeField(TAG_MSG_TYPE, messageDef.type, SOH_BYTE, charset) headerDef.encode(headerFields, body, isDirty, fieldsEncode, context) messageDef.encode(messageFields, body, isDirty, fieldsEncode, context, FIELDS_NOT_IN_BODY) trailerDef.encode(trailerFields, body, isDirty, fieldsEncode, context) - prefix.writeField(TAG_BODY_LENGTH, body.readableBytes(), charset) + prefix.writeField(TAG_BODY_LENGTH, body.readableBytes(), SOH_BYTE, charset) val buffer = Unpooled.wrappedBuffer(prefix, body) - buffer.writeChecksum() + buffer.writeChecksum(SOH_BYTE) messages += RawMessage( id = message.id, @@ -124,10 +127,16 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) val isDirty = isDirtyMode || (message.metadata[ENCODE_MODE_PROPERTY_NAME] == DIRTY_ENCODE_MODE) val buffer = message.body - val beginString = buffer.readField(TAG_BEGIN_STRING, charset, isDirty) { "Message starts with $it tag instead of BeginString ($TAG_BEGIN_STRING)" } - val bodyLengthString = buffer.readField(TAG_BODY_LENGTH, charset, isDirty) { "BeginString ($TAG_BEGIN_STRING) is followed by $it tag instead of BodyLength ($TAG_BODY_LENGTH)" } + val beginString = buffer.readField(TAG_BEGIN_STRING, decodeDelimiter, charset, isDirty) { + "Message starts with $it tag instead of BeginString ($TAG_BEGIN_STRING)" + } + val bodyLengthString = buffer.readField(TAG_BODY_LENGTH, decodeDelimiter, charset, isDirty) { + "BeginString ($TAG_BEGIN_STRING) is followed by $it tag instead of BodyLength ($TAG_BODY_LENGTH)" + } val bodyLength = bodyLengthString.toIntOrNull() ?: handleError(isDirty, context, "Wrong number value in integer field 'BodyLength'. Value: $bodyLengthString.", bodyLengthString) - val msgType = buffer.readField(TAG_MSG_TYPE, charset, isDirty) { "BodyLength ($TAG_BODY_LENGTH) is followed by $it tag instead of MsgType ($TAG_MSG_TYPE)" } + val msgType = buffer.readField(TAG_MSG_TYPE, decodeDelimiter, charset, isDirty) { + "BodyLength ($TAG_BODY_LENGTH) is followed by $it tag instead of MsgType ($TAG_MSG_TYPE)" + } val messageDef = messagesByTypeForDecode[msgType] ?: error("Unknown message type: $msgType") val header = headerDef.decode(buffer, messageDef, isDirty, fieldsDecode, context) @@ -207,13 +216,13 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) targetMap[name] = decodedValue } - private val prereadHeaderTags = arrayOf(8 /* BeginString */, 9 /* BodyLength */, 35 /* MsgType */) + private val preparedHeaderTags = arrayOf(8 /* BeginString */, 9 /* BodyLength */, 35 /* MsgType */) private fun Message.decode(source: ByteBuf, bodyDef: Message, isDirty: Boolean, dictionaryFields: Map, context: IReportingContext): MutableMap = mutableMapOf().also { map -> - val tagsSet: MutableSet = hashSetOf(*prereadHeaderTags) + val tagsSet: MutableSet = hashSetOf(*preparedHeaderTags) val usedComponents = mutableSetOf() - source.forEachField(charset, isDirty) { tag, value -> + source.forEachField(decodeDelimiter, charset, isDirty) { tag, value -> val field = get(tag) ?: if (isDirty) { when (this) { headerDef -> bodyDef[tag] ?: trailerDef[tag] @@ -291,9 +300,9 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) else -> error("Unsupported type: $primitiveType.") } - } catch (e: NumberFormatException) { + } catch (_: NumberFormatException) { handleError(isDirty, context, "Wrong number value in ${primitiveType.name} field '$name'. Value: $value.", value) - } catch (e: DateTimeParseException) { + } catch (_: DateTimeParseException) { handleError(isDirty, context, "Wrong date/time value in ${primitiveType.name} field '$name'. Value: $value.", value) } @@ -312,7 +321,7 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) var map: MutableMap? = null val tags: MutableSet = hashSetOf() - source.forEachField(charset, isDirty) { tag, value -> + source.forEachField(decodeDelimiter, charset, isDirty) { tag, value -> val field = get(tag) ?: return@forEachField false val group = if (tag == delimiter || tags.contains(tag) || map == null) { @@ -359,11 +368,11 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) // 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) + target.writeField(field.tag, value, SOH_BYTE, charset) return } } - } catch (e: DateTimeParseException) { + } catch (_: DateTimeParseException) { handleError(isDirty, context, "Wrong date/time value in ${field.primitiveType.name} field '$field.name'. Value: $value.", value) } } @@ -384,7 +393,7 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) field.values.isNotEmpty() && !field.values.contains(stringValue) -> handleError(isDirty, context, "Invalid value in enum field ${field.name}. Actual: $value. Valid values ${field.values}.") } - target.writeField(field.tag, stringValue, charset) + target.writeField(field.tag, stringValue, SOH_BYTE, charset) } field is Group && value is List<*> -> field.encode(value, target, isDirty, dictionaryFields, context) @@ -425,7 +434,7 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) error("List value with unspecified name. tag = $tag") } else { context.warning(DIRTY_MODE_WARNING_PREFIX + "Tag instead of field name. Field name: $fieldName. Field value: $value. Message body: $source") - target.writeField(tag, value, charset) + target.writeField(tag, value, SOH_BYTE, charset) } } else { error("Field does not exist in dictionary. Field name: $fieldName. Field value: $value. Message body: $source") @@ -435,7 +444,7 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) } private fun Group.encode(source: List<*>, target: ByteBuf, isDirty: Boolean, dictionaryFields: Map, context: IReportingContext) { - target.writeField(counter, source.size, charset) + target.writeField(counter, source.size, SOH_BYTE, charset) source.forEach { group -> check(group is Map<*, *>) { "Unsupported value in $name group: $group" } @@ -498,6 +507,9 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) ) : Field, FieldMap() companion object { + const val SOH_CHAR = '' + private const val SOH_BYTE = SOH_CHAR.code.toByte() + private const val HEADER = "header" private const val TRAILER = "trailer" private val FIELDS_NOT_IN_BODY = setOf(HEADER, TRAILER) diff --git a/src/main/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecSettings.kt b/src/main/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecSettings.kt index 3552807..8602a81 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecSettings.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecSettings.kt @@ -17,13 +17,24 @@ package com.exactpro.th2.codec.fixng import com.exactpro.th2.codec.api.IPipelineCodecSettings +import com.exactpro.th2.codec.fixng.FixNgCodec.Companion.SOH_CHAR +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.annotation.JsonDeserialize import java.nio.charset.Charset data class FixNgCodecSettings( val beginString: String = "FIXT.1.1", val dictionary: String, + @JsonDeserialize(using = CharsetDeserializer::class) val charset: Charset = Charsets.US_ASCII, + val decodeDelimiter: Char = SOH_CHAR, val dirtyMode: Boolean = false, val decodeValuesToStrings: Boolean = true, val decodeComponentsToNestedMaps: Boolean = true -) : IPipelineCodecSettings \ No newline at end of file +) : IPipelineCodecSettings + +object CharsetDeserializer : JsonDeserializer() { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Charset = Charset.forName(p.valueAsString) +} \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecSettingsTest.kt b/src/test/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecSettingsTest.kt new file mode 100644 index 0000000..3c7699e --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecSettingsTest.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.exactpro.th2.codec.fixng + +import com.exactpro.th2.common.schema.factory.CommonFactory +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class FixNgCodecSettingsTest { + + @Test + fun `decode settings`() { + val dictionary = "fix_dictionary.xml" + val settings: FixNgCodecSettings = CommonFactory.MAPPER.readValue(""" + { + "beginString": "FIXT.1.1", + "dictionary": "$dictionary", + "charset": "US-ASCII", + "decodeDelimiter": "\u0001", + "dirtyMode": false, + "decodeValuesToStrings": true, + "decodeComponentsToNestedMaps": true + } + """.trimIndent(), FixNgCodecSettings::class.java) + + assertEquals(FixNgCodecSettings(dictionary = dictionary), settings) + } +} \ No newline at end of file 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 138170d..f8df5d8 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecTest.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecTest.kt @@ -32,6 +32,8 @@ import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource import org.junit.jupiter.params.provider.ValueSource import java.math.BigDecimal import java.nio.charset.StandardCharsets @@ -43,7 +45,7 @@ class FixNgCodecTest { .getResourceAsStream("dictionary.xml") .use(XmlDictionaryStructureLoader()::load) - private val codec = FixNgCodec(dictionary, FixNgCodecSettings(dictionary = "", decodeValuesToStrings = false)) + private val codec = createCodec() private val reportingContext = object : IReportingContext { private val _warnings: MutableList = ArrayList() @@ -66,19 +68,28 @@ class FixNgCodecTest { @ParameterizedTest @ValueSource(booleans = [true, false]) - fun `simple encode from string values`(isDirty: Boolean) = encodeTest(MSG_CORRECT, dirtyMode = isDirty, encodeFromStringValues = true) + fun `simple encode from string values`(isDirty: Boolean) = + encodeTest(MSG_CORRECT, dirtyMode = isDirty, encodeFromStringValues = true) @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `simple decode`(isDirty: Boolean) = decodeTest(MSG_CORRECT, dirtyMode = isDirty) + @MethodSource("configs") + fun `simple decode`(isDirty: Boolean, delimiter: Char) = + decodeTest(MSG_CORRECT, dirtyMode = isDirty, delimiter = delimiter) @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `simple decode to string values`(isDirty: Boolean) = decodeTest(MSG_CORRECT, decodeToStringValues = true, dirtyMode = isDirty) + @MethodSource("configs") + fun `simple decode to string values`(isDirty: Boolean, delimiter: Char) = + decodeTest(MSG_CORRECT, dirtyMode = isDirty, delimiter = delimiter, decodeToStringValues = true) @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `simple decode with no body`(isDirty: Boolean) = decodeTest(MSG_CORRECT_WITHOUT_BODY, expectedMessage = expectedMessageWithoutBody, dirtyMode = isDirty) + @MethodSource("configs") + fun `simple decode with no body`(isDirty: Boolean, delimiter: Char) = + decodeTest( + MSG_CORRECT_WITHOUT_BODY, + dirtyMode = isDirty, + delimiter = delimiter, + expectedMessage = expectedMessageWithoutBody + ) @ParameterizedTest @ValueSource(booleans = [true, false]) @@ -87,41 +98,66 @@ class FixNgCodecTest { encodeTest(MSG_ADDITIONAL_FIELD_DICT, "Unexpected field in message. Field name: CFICode. Field value: 12345.", dirtyMode = isDirty) } - @Test - fun `decode with addition field that exists in dictionary (dirty)`() { + @ParameterizedTest + @ValueSource(chars = ['', '|']) + fun `decode with addition field that exists in dictionary (dirty)`(delimiter: Char) { parsedBody["CFICode"] = "12345" - decodeTest(MSG_ADDITIONAL_FIELD_DICT, "Unexpected field in message. Field name: CFICode. Field value: 12345.", dirtyMode = true) + decodeTest( + MSG_ADDITIONAL_FIELD_DICT, + dirtyMode = true, + delimiter = delimiter, + "Unexpected field in message. Field name: CFICode. Field value: 12345." + ) } - @Test - fun `decode with addition field that exists in dictionary (non dirty)`() { + @ParameterizedTest + @ValueSource(chars = ['', '|']) + fun `decode with addition field that exists in dictionary (non dirty)`(delimiter: Char) { parsedBody["CFICode"] = "12345" // note: Unknown tag in the message causes the processing of messages to stop and moves on to the next part of // the message. As a result, required tags remain unread, which leads to the following error. - decodeTest(MSG_ADDITIONAL_FIELD_DICT, "Required tag missing. Tag: 10.", dirtyMode = false) + decodeTest( + MSG_ADDITIONAL_FIELD_DICT, + dirtyMode = false, + delimiter = delimiter, + "Required tag missing. Tag: 10." + ) } - @Test - fun `encode with addition field that does not exists in dictionary`() { + @ParameterizedTest + @ValueSource(chars = ['', '|']) + fun `encode with addition field that does not exists in dictionary`(delimiter: Char) { parsedBody["UNKNOWN_FIELD"] = "test_value" - assertThatThrownBy { codec.encode(MessageGroup(listOf(parsedMessage)), reportingContext) } + assertThatThrownBy { createCodec(delimiter).encode(MessageGroup(listOf(parsedMessage)), reportingContext) } .isInstanceOf(IllegalStateException::class.java) .message() .startsWith("Field does not exist in dictionary. Field name: UNKNOWN_FIELD. Field value: test_value.") } - @Test - fun `decode with addition field that does not exists in dictionary (dirty)`() { + @ParameterizedTest + @ValueSource(chars = ['', '|']) + fun `decode with addition field that does not exists in dictionary (dirty)`(delimiter: Char) { parsedBody["9999"] = "54321" - decodeTest(MSG_ADDITIONAL_FIELD_NO_DICT, "Field does not exist in dictionary. Field tag: 9999. Field value: 54321.", dirtyMode = true) + decodeTest( + MSG_ADDITIONAL_FIELD_NO_DICT, + dirtyMode = true, + delimiter = delimiter, + "Field does not exist in dictionary. Field tag: 9999. Field value: 54321." + ) } - @Test - fun `decode with addition field that does not exists in dictionary (non dirty)`() { + @ParameterizedTest + @ValueSource(chars = ['', '|']) + fun `decode with addition field that does not exists in dictionary (non dirty)`(delimiter: Char) { parsedBody["9999"] = "54321" // note: Unknown tag in the message causes the processing of messages to stop and moves on to the next part of // the message. As a result, required tags remain unread, which leads to the following error. - decodeTest(MSG_ADDITIONAL_FIELD_NO_DICT, "Required tag missing. Tag: 10.", dirtyMode = false) + decodeTest( + MSG_ADDITIONAL_FIELD_NO_DICT, + dirtyMode = false, + delimiter = delimiter, + "Required tag missing. Tag: 10." + ) } @Test @@ -144,10 +180,15 @@ class FixNgCodecTest { } @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `decode with required field removed`(isDirty: Boolean) { + @MethodSource("configs") + fun `decode with required field removed`(isDirty: Boolean, delimiter: Char) { parsedBody.remove("ExecID") - decodeTest(MSG_REQUIRED_FIELD_REMOVED, "Required tag missing. Tag: 17.", dirtyMode = isDirty) + decodeTest( + MSG_REQUIRED_FIELD_REMOVED, + dirtyMode = isDirty, + delimiter = delimiter, + "Required tag missing. Tag: 17." + ) } @ParameterizedTest @@ -159,11 +200,16 @@ class FixNgCodecTest { } @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `decode with required delimiter field in group removed in first entry`(isDirty: Boolean) { + @MethodSource("configs") + fun `decode with required delimiter field in group removed in first entry`(isDirty: Boolean, delimiter: Char) { @Suppress("UNCHECKED_CAST") ((parsedBody["TradingParty"] as Map)["NoPartyIDs"] as List>)[0].remove("PartyID") - decodeTest(MSG_DELIMITER_FIELD_IN_GROUP_REMOVED_IN_FIRST_ENTRY, "Field PartyIDSource (447) appears before delimiter (448)", dirtyMode = isDirty) + decodeTest( + MSG_DELIMITER_FIELD_IN_GROUP_REMOVED_IN_FIRST_ENTRY, + dirtyMode = isDirty, + delimiter = delimiter, + "Field PartyIDSource (447) appears before delimiter (448)" + ) } @ParameterizedTest @@ -175,11 +221,16 @@ class FixNgCodecTest { } @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `decode with required delimiter field in group removed in second entry`(isDirty: Boolean) { + @MethodSource("configs") + fun `decode with required delimiter field in group removed in second entry`(isDirty: Boolean, delimiter: Char) { @Suppress("UNCHECKED_CAST") ((parsedBody["TradingParty"] as Map)["NoPartyIDs"] as List>)[1].remove("PartyID") - decodeTest(MSG_DELIMITER_FIELD_IN_GROUP_REMOVED_IN_SECOND_ENTRY, "Field PartyIDSource (447) appears before delimiter (448)", dirtyMode = isDirty) + decodeTest( + MSG_DELIMITER_FIELD_IN_GROUP_REMOVED_IN_SECOND_ENTRY, + dirtyMode = isDirty, + delimiter = delimiter, + "Field PartyIDSource (447) appears before delimiter (448)" + ) } @ParameterizedTest @@ -190,10 +241,15 @@ class FixNgCodecTest { } @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `decode with wrong enum value`(isDirty: Boolean) { + @MethodSource("configs") + fun `decode with wrong enum value`(isDirty: Boolean, delimiter: Char) { parsedBody["ExecType"] = 'X' - decodeTest(MSG_WRONG_ENUM, "Invalid value in enum field ExecType. Actual: X.", dirtyMode = isDirty) + decodeTest( + MSG_WRONG_ENUM, + dirtyMode = isDirty, + delimiter = delimiter, + "Invalid value in enum field ExecType. Actual: X." + ) } @ParameterizedTest @@ -218,10 +274,15 @@ class FixNgCodecTest { } @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `decode with wrong value type`(isDirty: Boolean) { + @MethodSource("configs") + fun `decode with wrong value type`(isDirty: Boolean, delimiter: Char) { parsedBody["LeavesQty"] = "Five" - decodeTest(MSG_WRONG_TYPE, "Wrong number value in java.math.BigDecimal field 'LeavesQty'. Value: Five.", dirtyMode = isDirty) + decodeTest( + MSG_WRONG_TYPE, + dirtyMode = isDirty, + delimiter = delimiter, + "Wrong number value in java.math.BigDecimal field 'LeavesQty'. Value: Five." + ) } @ParameterizedTest @@ -231,16 +292,18 @@ class FixNgCodecTest { encodeTest(MSG_EMPTY_VAL, "Empty value in the field 'Account'.", dirtyMode = isDirty) } - @Test - fun `decode with empty value (dirty)`() { + @ParameterizedTest + @ValueSource(chars = ['', '|']) + fun `decode with empty value (dirty)`(delimiter: Char) { parsedBody["Account"] = "" - decodeTest(MSG_EMPTY_VAL, "Empty value in the field 'Account'.", dirtyMode = true) + decodeTest(MSG_EMPTY_VAL, dirtyMode = true, delimiter = delimiter, "Empty value in the field 'Account'.") } - @Test - fun `decode with empty value (non dirty)`() { + @ParameterizedTest + @ValueSource(chars = ['', '|']) + fun `decode with empty value (non dirty)`(delimiter: Char) { parsedBody["Account"] = "" - decodeTest(MSG_EMPTY_VAL, "No valid value at offset: 235", dirtyMode = false) + decodeTest(MSG_EMPTY_VAL, dirtyMode = false, delimiter = delimiter, "No valid value at offset: 235") } @ParameterizedTest @@ -251,10 +314,15 @@ class FixNgCodecTest { } @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `decode with non printable characters`(isDirty: Boolean) { + @MethodSource("configs") + fun `decode with non printable characters`(isDirty: Boolean, delimiter: Char) { parsedBody["Account"] = "test\taccount" - decodeTest(MSG_NON_PRINTABLE, "Non printable characters in the field 'Account'.", dirtyMode = isDirty) + decodeTest( + MSG_NON_PRINTABLE, + dirtyMode = isDirty, + delimiter = delimiter, + "Non printable characters in the field 'Account'." + ) } @ParameterizedTest @@ -282,36 +350,61 @@ class FixNgCodecTest { encodeTest(MSG_REQUIRED_HEADER_REMOVED, dirtyMode = isDirty) } - @Test - fun `tag appears out of order (dirty)`() { + @ParameterizedTest + @ValueSource(chars = ['', '|']) + fun `tag appears out of order (dirty)`(delimiter: Char) { @Suppress("UNCHECKED_CAST") val trailer = parsedBody["trailer"] as MutableMap trailer["LegUnitOfMeasure"] = "500" - decodeTest(MSG_TAG_OUT_OF_ORDER, "Unexpected field in message. Field name: LegUnitOfMeasure. Field value: 500", dirtyMode = true) + decodeTest( + MSG_TAG_OUT_OF_ORDER, + dirtyMode = true, + delimiter = delimiter, + "Unexpected field in message. Field name: LegUnitOfMeasure. Field value: 500" + ) } - @Test - fun `tag appears out of order (non dirty)`() = decodeTest(MSG_TAG_OUT_OF_ORDER, "Tag appears out of order: 999", dirtyMode = false) + @ParameterizedTest + @ValueSource(chars = ['', '|']) + fun `tag appears out of order (non dirty)`(delimiter: Char) { + decodeTest(MSG_TAG_OUT_OF_ORDER, dirtyMode = false, delimiter = delimiter, "Tag appears out of order: 999") + } @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `decode nested components`(isDirty: Boolean) = - decodeTest(MSG_NESTED_REQ_COMPONENTS, expectedMessage = parsedMessageWithNestedComponents, dirtyMode = isDirty) + @MethodSource("configs") + fun `decode nested components`(isDirty: Boolean, delimiter: Char) = + decodeTest( + MSG_NESTED_REQ_COMPONENTS, + dirtyMode = isDirty, + delimiter = delimiter, + expectedMessage = parsedMessageWithNestedComponents + ) @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `decode with missing req field in req nested component`(isDirty: Boolean) { + @MethodSource("configs") + fun `decode with missing req field in req nested component`(isDirty: Boolean, delimiter: Char) { @Suppress("UNCHECKED_CAST") (parsedBodyWithNestedComponents["OuterComponent"] as MutableMap>)["InnerComponent"]?.remove("OrdType") - decodeTest(MSG_NESTED_REQ_COMPONENTS_MISSED_REQ, expectedErrorText = "Required tag missing. Tag: 40.", expectedMessage = parsedMessageWithNestedComponents, dirtyMode = isDirty) + decodeTest( + MSG_NESTED_REQ_COMPONENTS_MISSED_REQ, + dirtyMode = isDirty, + delimiter = delimiter, + expectedErrorText = "Required tag missing. Tag: 40.", + expectedMessage = parsedMessageWithNestedComponents + ) } @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `decode with missing optional field in req nested component`(isDirty: Boolean) { + @MethodSource("configs") + fun `decode with missing optional field in req nested component`(isDirty: Boolean, delimiter: Char) { @Suppress("UNCHECKED_CAST") (parsedBodyWithNestedComponents["OuterComponent"] as MutableMap>)["InnerComponent"]?.remove("Text") - decodeTest(MSG_NESTED_REQ_COMPONENTS_MISSED_OPTIONAL, expectedMessage = parsedMessageWithNestedComponents, dirtyMode = isDirty) + decodeTest( + MSG_NESTED_REQ_COMPONENTS_MISSED_OPTIONAL, + dirtyMode = isDirty, + delimiter = delimiter, + expectedMessage = parsedMessageWithNestedComponents + ) } private fun convertToOptionalComponent(): ParsedMessage { @@ -323,41 +416,58 @@ class FixNgCodecTest { } @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `decode nested optional components`(isDirty: Boolean) { + @MethodSource("configs") + fun `decode nested optional components`(isDirty: Boolean, delimiter: Char) { val message = convertToOptionalComponent() - decodeTest(MSG_NESTED_OPT_COMPONENTS, expectedMessage = message, dirtyMode = isDirty) + decodeTest(MSG_NESTED_OPT_COMPONENTS, dirtyMode = isDirty, delimiter = delimiter, expectedMessage = message) } @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `decode with missing req field in opt nested component`(isDirty: Boolean) { + @MethodSource("configs") + fun `decode with missing req field in opt nested component`(isDirty: Boolean, delimiter: Char) { val message = convertToOptionalComponent() @Suppress("UNCHECKED_CAST") (parsedBodyWithNestedComponents["OuterComponent"] as MutableMap>)["InnerComponent"]?.remove("OrdType") - decodeTest(MSG_NESTED_OPT_COMPONENTS_MISSED_REQ, expectedErrorText = "Required tag missing. Tag: 40.", expectedMessage = message, dirtyMode = isDirty) + decodeTest( + MSG_NESTED_OPT_COMPONENTS_MISSED_REQ, + dirtyMode = isDirty, + delimiter = delimiter, + expectedErrorText = "Required tag missing. Tag: 40.", + expectedMessage = message + ) } @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `decode with missing all fields in opt nested component`(isDirty: Boolean) { + @MethodSource("configs") + fun `decode with missing all fields in opt nested component`(isDirty: Boolean, delimiter: Char) { val message = convertToOptionalComponent() @Suppress("UNCHECKED_CAST") (parsedBodyWithNestedComponents["OuterComponent"] as MutableMap>).remove("InnerComponent") - decodeTest(MSG_NESTED_OPT_COMPONENTS_MISSED_ALL_FIELDS, expectedErrorText = "Required tag missing. Tag: 40.", expectedMessage = message, dirtyMode = isDirty) + decodeTest( + MSG_NESTED_OPT_COMPONENTS_MISSED_ALL_FIELDS, + dirtyMode = isDirty, + delimiter = delimiter, + expectedErrorText = "Required tag missing. Tag: 40.", + expectedMessage = message + ) } @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `decode with missing all fields in inner and outer nested components`(isDirty: Boolean) { + @MethodSource("configs") + fun `decode with missing all fields in inner and outer nested components`(isDirty: Boolean, delimiter: Char) { val message = convertToOptionalComponent() parsedBodyWithNestedComponents.remove("OuterComponent") - decodeTest(MSG_NESTED_OPT_COMPONENTS_MISSED_ALL_FIELDS_INNER_AND_OUTER, expectedMessage = message, dirtyMode = isDirty) + decodeTest( + MSG_NESTED_OPT_COMPONENTS_MISSED_ALL_FIELDS_INNER_AND_OUTER, + dirtyMode = isDirty, + delimiter = delimiter, + expectedMessage = message + ) } @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `decode with missing req fields in both inner and outer components`(isDirty: Boolean) { + @MethodSource("configs") + fun `decode with missing req fields in both inner and outer components`(isDirty: Boolean, delimiter: Char) { val message = convertToOptionalComponent() @Suppress("UNCHECKED_CAST") (parsedBodyWithNestedComponents["OuterComponent"] as MutableMap>).remove("LeavesQty") @@ -365,10 +475,11 @@ class FixNgCodecTest { (parsedBodyWithNestedComponents["OuterComponent"] as MutableMap>)["InnerComponent"]!!.remove("OrdType") decodeTest( MSG_NESTED_OPT_COMPONENTS_MISSED_ALL_OUTER_FIELDS_AND_REQ_INNER_FIELD, + dirtyMode = isDirty, + delimiter = delimiter, expectedErrorText = "Required tag missing. Tag: 40.", expectedSecondErrorText = "Required tag missing. Tag: 151.", - expectedMessage = message, - dirtyMode = isDirty + expectedMessage = message ) } @@ -377,8 +488,21 @@ class FixNgCodecTest { fun `encode nested groups`(isDirty: Boolean) = encodeTest(MSG_NESTED_GROUPS, dirtyMode = isDirty, parsedMessage = parsedMessageWithNestedGroups) @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun `decode nested groups`(isDirty: Boolean) = decodeTest(MSG_NESTED_GROUPS, expectedMessage = parsedMessageWithNestedGroups, dirtyMode = isDirty) + @MethodSource("configs") + fun `decode nested groups`(isDirty: Boolean, delimiter: Char) = decodeTest( + MSG_NESTED_GROUPS, + dirtyMode = isDirty, + delimiter = delimiter, + expectedMessage = parsedMessageWithNestedGroups + ) + + private fun createCodec(delimiter: Char = '', decodeValuesToStrings: Boolean = false): FixNgCodec { + return FixNgCodec(dictionary, FixNgCodecSettings( + dictionary = "", + decodeValuesToStrings = decodeValuesToStrings, + decodeDelimiter = delimiter + )) + } private fun encodeTest( expectedRawMessage: String, @@ -452,15 +576,17 @@ class FixNgCodecTest { private fun decodeTest( rawMessageString: String, + dirtyMode: Boolean, + delimiter: Char, expectedErrorText: String? = null, expectedSecondErrorText: String? = null, expectedMessage: ParsedMessage = parsedMessage, - dirtyMode: Boolean, decodeToStringValues: Boolean = false ) { if (dirtyMode) { decodeTestDirty( - rawMessageString, + rawMessageString.replaceSoh(delimiter), + delimiter, expectedErrorText, expectedSecondErrorText, expectedMessage, @@ -468,7 +594,8 @@ class FixNgCodecTest { ) } else { decodeTestNonDirty( - rawMessageString, + rawMessageString.replaceSoh(delimiter), + delimiter, expectedErrorText, expectedMessage, decodeToStringValues @@ -478,10 +605,11 @@ class FixNgCodecTest { private fun decodeTestDirty( rawMessageString: String, + delimiter: Char, expectedErrorText: String? = null, expectedSecondErrorText: String? = null, expectedMessage: ParsedMessage = parsedMessage, - decodeToStringValues: Boolean = false + decodeValuesToStrings: Boolean = false ) { val expectedBody = expectedMessage.body val rawMessage = RawMessage( @@ -491,13 +619,13 @@ class FixNgCodecTest { body = Unpooled.wrappedBuffer(rawMessageString.toByteArray(Charsets.US_ASCII)) ) - val codec = if (decodeToStringValues) FixNgCodec(dictionary, FixNgCodecSettings(dictionary = "")) else this.codec + val codec = createCodec(delimiter, decodeValuesToStrings) val decodedGroup = codec.decode(MessageGroup(listOf(rawMessage)), reportingContext) val parsedMessage = decodedGroup.messages.single() as ParsedMessage // 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 (decodeToStringValues) convertValuesToString(expectedBody) else expectedBody + val expected = if (decodeValuesToStrings) convertValuesToString(expectedBody) else expectedBody assertThat(parsedMessage.body) .usingRecursiveComparison() @@ -519,9 +647,10 @@ class FixNgCodecTest { private fun decodeTestNonDirty( rawMessageString: String, + delimiter: Char, expectedErrorText: String? = null, expectedMessage: ParsedMessage = parsedMessage, - decodeToStringValues: Boolean = false + decodeValuesToStrings: Boolean = false ) { val expectedBody = expectedMessage.body val rawMessage = RawMessage( @@ -531,7 +660,7 @@ class FixNgCodecTest { body = Unpooled.wrappedBuffer(rawMessageString.toByteArray(Charsets.US_ASCII)) ) - val codec = if (decodeToStringValues) FixNgCodec(dictionary, FixNgCodecSettings(dictionary = "")) else this.codec + val codec = createCodec(delimiter, decodeValuesToStrings) if (expectedErrorText != null) { assertThatThrownBy { codec.decode(MessageGroup(listOf(rawMessage)), reportingContext) @@ -539,7 +668,7 @@ class FixNgCodecTest { } else { val decodedGroup = codec.decode(MessageGroup(listOf(rawMessage)), reportingContext) val parsedMessage = decodedGroup.messages.single() as ParsedMessage - val expected = if (decodeToStringValues) convertValuesToString(expectedBody) else expectedBody + val expected = if (decodeValuesToStrings) convertValuesToString(expectedBody) else expectedBody assertThat(parsedMessage.body) .usingRecursiveComparison() .isEqualTo(expected) @@ -706,20 +835,20 @@ class FixNgCodecTest { companion object { private const val DIRTY_MODE_WARNING_PREFIX = "Dirty mode WARNING: " - private const val MSG_CORRECT = "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=191\u0001" - private const val MSG_CORRECT_WITHOUT_BODY = "8=FIX.4.2\u00019=55\u000135=0\u000134=125\u000149=MZHOT0\u000152=20240801-08:03:01.229\u000156=INET\u000110=039\u0001" - private const val MSG_ADDITIONAL_FIELD_DICT = "8=FIXT.1.1\u00019=305\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\u0001461=12345\u000110=143\u0001" - private const val MSG_ADDITIONAL_FIELD_NO_DICT = "8=FIXT.1.1\u00019=305\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\u00019999=54321\u000110=097\u0001" - private const val MSG_ADDITIONAL_FIELD_TAG = "8=FIXT.1.1\u00019=306\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\u00019999=12345\u000110=217\u0001" - private const val MSG_REQUIRED_FIELD_REMOVED = "8=FIXT.1.1\u00019=282\u000135=8\u000149=SENDER\u000156=RECEIVER\u000134=10947\u000152=20230419-10:36:07.415088\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=060\u0001" - private const val MSG_DELIMITER_FIELD_IN_GROUP_REMOVED_IN_FIRST_ENTRY = "8=FIXT.1.1\u00019=280\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\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=061\u0001" - private const val MSG_DELIMITER_FIELD_IN_GROUP_REMOVED_IN_SECOND_ENTRY = "8=FIXT.1.1\u00019=289\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\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=180\u0001" - private const val MSG_WRONG_ENUM = "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=X\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=231\u0001" - private const val MSG_WRONG_TYPE = "8=FIXT.1.1\u00019=296\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=Five\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=181\u0001" - private const val MSG_EMPTY_VAL = "8=FIXT.1.1\u00019=291\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=\u000140=A\u000159=0\u000154=B\u000155=ABC\u000138=500\u000144=1000\u000147=500\u000160=20180205-10:38:08.000008\u000110=251\u0001" - 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 const val MSG_CORRECT = "8=FIXT.1.19=29535=849=SENDER56=RECEIVER34=1094752=20230419-10:36:07.41508817=49550466211=zSuNbrBIZyVljs41=zSuNbrBIZyVljs37=49415882150=039=0151=50014=50048=NWDR22=8453=2448=NGALL1FX01447=D452=76448=0447=P452=31=test40=A59=054=B55=ABC38=50044=100047=50060=20180205-10:38:08.00000810=191" + private const val MSG_CORRECT_WITHOUT_BODY = "8=FIX.4.29=5535=034=12549=MZHOT052=20240801-08:03:01.22956=INET10=039" + private const val MSG_ADDITIONAL_FIELD_DICT = "8=FIXT.1.19=30535=849=SENDER56=RECEIVER34=1094752=20230419-10:36:07.41508817=49550466211=zSuNbrBIZyVljs41=zSuNbrBIZyVljs37=49415882150=039=0151=50014=50048=NWDR22=8453=2448=NGALL1FX01447=D452=76448=0447=P452=31=test40=A59=054=B55=ABC38=50044=100047=50060=20180205-10:38:08.000008461=1234510=143" + private const val MSG_ADDITIONAL_FIELD_NO_DICT = "8=FIXT.1.19=30535=849=SENDER56=RECEIVER34=1094752=20230419-10:36:07.41508817=49550466211=zSuNbrBIZyVljs41=zSuNbrBIZyVljs37=49415882150=039=0151=50014=50048=NWDR22=8453=2448=NGALL1FX01447=D452=76448=0447=P452=31=test40=A59=054=B55=ABC38=50044=100047=50060=20180205-10:38:08.0000089999=5432110=097" + private const val MSG_ADDITIONAL_FIELD_TAG = "8=FIXT.1.19=30635=849=SENDER56=RECEIVER34=1094752=20230419-10:36:07.41508817=49550466211=zSuNbrBIZyVljs41=zSuNbrBIZyVljs37=49415882150=039=0151=50014=50048=NWDR22=8453=2448=NGALL1FX01447=D452=76448=0447=P452=31=test40=A59=054=B55=ABC38=50044=100047=50060=20180205-10:38:08.0000089999=1234510=217" + private const val MSG_REQUIRED_FIELD_REMOVED = "8=FIXT.1.19=28235=849=SENDER56=RECEIVER34=1094752=20230419-10:36:07.41508811=zSuNbrBIZyVljs41=zSuNbrBIZyVljs37=49415882150=039=0151=50014=50048=NWDR22=8453=2448=NGALL1FX01447=D452=76448=0447=P452=31=test40=A59=054=B55=ABC38=50044=100047=50060=20180205-10:38:08.00000810=060" + private const val MSG_DELIMITER_FIELD_IN_GROUP_REMOVED_IN_FIRST_ENTRY = "8=FIXT.1.19=28035=849=SENDER56=RECEIVER34=1094752=20230419-10:36:07.41508817=49550466211=zSuNbrBIZyVljs41=zSuNbrBIZyVljs37=49415882150=039=0151=50014=50048=NWDR22=8453=2447=D452=76448=0447=P452=31=test40=A59=054=B55=ABC38=50044=100047=50060=20180205-10:38:08.00000810=061" + private const val MSG_DELIMITER_FIELD_IN_GROUP_REMOVED_IN_SECOND_ENTRY = "8=FIXT.1.19=28935=849=SENDER56=RECEIVER34=1094752=20230419-10:36:07.41508817=49550466211=zSuNbrBIZyVljs41=zSuNbrBIZyVljs37=49415882150=039=0151=50014=50048=NWDR22=8453=2448=NGALL1FX01447=D452=76447=P452=31=test40=A59=054=B55=ABC38=50044=100047=50060=20180205-10:38:08.00000810=180" + private const val MSG_WRONG_ENUM = "8=FIXT.1.19=29535=849=SENDER56=RECEIVER34=1094752=20230419-10:36:07.41508817=49550466211=zSuNbrBIZyVljs41=zSuNbrBIZyVljs37=49415882150=X39=0151=50014=50048=NWDR22=8453=2448=NGALL1FX01447=D452=76448=0447=P452=31=test40=A59=054=B55=ABC38=50044=100047=50060=20180205-10:38:08.00000810=231" + private const val MSG_WRONG_TYPE = "8=FIXT.1.19=29635=849=SENDER56=RECEIVER34=1094752=20230419-10:36:07.41508817=49550466211=zSuNbrBIZyVljs41=zSuNbrBIZyVljs37=49415882150=039=0151=Five14=50048=NWDR22=8453=2448=NGALL1FX01447=D452=76448=0447=P452=31=test40=A59=054=B55=ABC38=50044=100047=50060=20180205-10:38:08.00000810=181" + private const val MSG_EMPTY_VAL = "8=FIXT.1.19=29135=849=SENDER56=RECEIVER34=1094752=20230419-10:36:07.41508817=49550466211=zSuNbrBIZyVljs41=zSuNbrBIZyVljs37=49415882150=039=0151=50014=50048=NWDR22=8453=2448=NGALL1FX01447=D452=76448=0447=P452=31=40=A59=054=B55=ABC38=50044=100047=50060=20180205-10:38:08.00000810=251" + private const val MSG_NON_PRINTABLE = "8=FIXT.1.19=30335=849=SENDER56=RECEIVER34=1094752=20230419-10:36:07.41508817=49550466211=zSuNbrBIZyVljs41=zSuNbrBIZyVljs37=49415882150=039=0151=50014=50048=NWDR22=8453=2448=NGALL1FX01447=D452=76448=0447=P452=31=test\taccount40=A59=054=B55=ABC38=50044=100047=50060=20180205-10:38:08.00000810=171" + private const val MSG_REQUIRED_HEADER_REMOVED = "8=FIXT.1.19=23635=817=49550466211=zSuNbrBIZyVljs41=zSuNbrBIZyVljs37=49415882150=039=0151=50014=50048=NWDR22=8453=2448=NGALL1FX01447=D452=76448=0447=P452=31=test40=A59=054=B55=ABC38=50044=100047=50060=20180205-10:38:08.00000810=050" + private const val MSG_TAG_OUT_OF_ORDER = "8=FIXT.1.19=29535=849=SENDER56=RECEIVER34=1094752=20230419-10:36:07.41508817=49550466211=zSuNbrBIZyVljs41=zSuNbrBIZyVljs37=49415882150=039=0151=50014=50048=NWDR22=8453=2448=NGALL1FX01447=D452=76448=0447=P452=31=test40=A59=054=B55=ABC38=50044=100047=50060=20180205-10:38:08.00000810=000999=500" private const val MSG_NESTED_REQ_COMPONENTS = "8=FIXT.1.19=5935=TEST_149=MZHOT056=INET34=12558=text_140=1151=123410=191" private const val MSG_NESTED_REQ_COMPONENTS_MISSED_REQ = "8=FIXT.1.19=5935=TEST_149=MZHOT056=INET34=12558=text_1151=123410=191" @@ -732,5 +861,15 @@ class FixNgCodecTest { private const val MSG_NESTED_OPT_COMPONENTS_MISSED_ALL_OUTER_FIELDS_AND_REQ_INNER_FIELD = "8=FIXT.1.19=5935=TEST_249=MZHOT056=INET34=12558=text_110=191" private const val MSG_NESTED_GROUPS = "8=FIXT.1.19=8835=TEST_349=MZHOT056=INET34=12573=2398=3399=1399=2399=3398=3399=3399=2399=110=211" + + @JvmStatic + fun configs() = listOf( + Arguments.of(true, ''), + Arguments.of(true, '|'), + Arguments.of(false, ''), + Arguments.of(false, '|'), + ) + + private fun String.replaceSoh(value: Char) = if (value == '') this else replace('', value) } } \ No newline at end of file