diff --git a/artio-codecs/src/main/java/uk/co/real_logic/artio/dictionary/generation/CodecConfiguration.java b/artio-codecs/src/main/java/uk/co/real_logic/artio/dictionary/generation/CodecConfiguration.java index 6870d3173d..9d2ef87c6e 100644 --- a/artio-codecs/src/main/java/uk/co/real_logic/artio/dictionary/generation/CodecConfiguration.java +++ b/artio-codecs/src/main/java/uk/co/real_logic/artio/dictionary/generation/CodecConfiguration.java @@ -52,6 +52,7 @@ public final class CodecConfiguration * without checking whether the field is set. */ public static final String WRAP_EMPTY_BUFFER = "fix.codecs.wrap_empty_buffer"; + public static final String ALLOW_EMPTY_TAGS = "fix.codecs.allow_empty_tags"; public static final String PARENT_PACKAGE_PROPERTY = "fix.codecs.parent_package"; public static final String FLYWEIGHTS_ENABLED_PROPERTY = "fix.codecs.flyweight"; public static final String REJECT_UNKNOWN_ENUM_VALUE_PROPERTY = "reject.unknown.enum.value"; @@ -63,6 +64,7 @@ public final class CodecConfiguration private String parentPackage = System.getProperty(PARENT_PACKAGE_PROPERTY, DEFAULT_PARENT_PACKAGE); private boolean flyweightsEnabled = Boolean.getBoolean(FLYWEIGHTS_ENABLED_PROPERTY); private boolean wrapEmptyBuffer = Boolean.getBoolean(WRAP_EMPTY_BUFFER); + private boolean allowEmptyTags = Boolean.getBoolean(ALLOW_EMPTY_TAGS); private boolean fixTagsInJavadoc = Boolean.parseBoolean(System.getProperty( FIX_TAGS_IN_JAVADOC, DEFAULT_FIX_TAGS_IN_JAVADOC)); private SharedCodecConfiguration sharedCodecConfiguration; @@ -129,6 +131,21 @@ public CodecConfiguration wrapEmptyBuffer(final boolean wrapEmptyBuffer) return this; } + /** + * Suppresses checks for empty tags, eg |1234=| + * instead of rejecting when tag is empty, treat the tag as absent + * + * Defaults to the value of {@link #ALLOW_EMPTY_TAGS} system property. + * + * @param allowEmptyTags true to suppress check of empty tags, false to keep checking (default) + * @return this + */ + public CodecConfiguration allowEmptyTags(final boolean allowEmptyTags) + { + this.allowEmptyTags = allowEmptyTags; + return this; + } + /** * Allow duplicate fields. Executable documentation can be found in the test "DuplicateFieldsTest". * @@ -248,6 +265,11 @@ boolean wrapEmptyBuffer() return wrapEmptyBuffer; } + public boolean allowEmptyTags() + { + return allowEmptyTags; + } + String codecRejectUnknownEnumValueEnabled() { return codecRejectUnknownEnumValueEnabled; diff --git a/artio-codecs/src/main/java/uk/co/real_logic/artio/dictionary/generation/CodecGenerator.java b/artio-codecs/src/main/java/uk/co/real_logic/artio/dictionary/generation/CodecGenerator.java index 6964b4e895..0358503b71 100644 --- a/artio-codecs/src/main/java/uk/co/real_logic/artio/dictionary/generation/CodecGenerator.java +++ b/artio-codecs/src/main/java/uk/co/real_logic/artio/dictionary/generation/CodecGenerator.java @@ -180,6 +180,7 @@ private static void generateDictionary( RejectUnknownEnumValue.class, false, configuration.wrapEmptyBuffer(), + configuration.allowEmptyTags(), codecRejectUnknownEnumValueEnabled, configuration.fixTagsInJavadoc()).generate(); @@ -202,6 +203,7 @@ private static void generateDictionary( RejectUnknownEnumValue.class, true, configuration.wrapEmptyBuffer(), + configuration.allowEmptyTags(), codecRejectUnknownEnumValueEnabled, configuration.fixTagsInJavadoc()).generate(); } diff --git a/artio-codecs/src/main/java/uk/co/real_logic/artio/dictionary/generation/DecoderGenerator.java b/artio-codecs/src/main/java/uk/co/real_logic/artio/dictionary/generation/DecoderGenerator.java index 54fc32a812..6c0ddb111e 100644 --- a/artio-codecs/src/main/java/uk/co/real_logic/artio/dictionary/generation/DecoderGenerator.java +++ b/artio-codecs/src/main/java/uk/co/real_logic/artio/dictionary/generation/DecoderGenerator.java @@ -119,6 +119,11 @@ static String decoderClassName(final String name) * Wrap empty buffer instead of throwing an exception if an optional string is unset. */ private final boolean wrapEmptyBuffer; + /** + * Do not reject messages when there's an empty tag: instead, treat the field as absent + */ + private final boolean allowEmptyTags; + DecoderGenerator( final Dictionary dictionary, @@ -132,6 +137,7 @@ static String decoderClassName(final String name) final Class rejectUnknownEnumValueClass, final boolean flyweightsEnabled, final boolean wrapEmptyBuffer, + final boolean allowEmptyTags, final String codecRejectUnknownEnumValueEnabled, final boolean fixTagsInJavadoc) { @@ -140,6 +146,7 @@ static String decoderClassName(final String name) this.initialBufferSize = initialBufferSize; this.encoderPackage = encoderPackage; this.wrapEmptyBuffer = wrapEmptyBuffer; + this.allowEmptyTags = allowEmptyTags; } public void generate() @@ -1770,11 +1777,7 @@ private String generateDecodePrefix( " invalidTagId = tag;\n" + " rejectReason = " + INVALID_TAG_NUMBER + ";\n" + " }\n" + - " else if (valueLength == 0)\n" + - " {\n" + - " invalidTagId = tag;\n" + - " rejectReason = " + TAG_SPECIFIED_WITHOUT_A_VALUE + ";\n" + - " }\n" + + emptyTagValidation() + headerValidation(isHeader) + (isGroup ? "" : " if (!alreadyVisitedFields.add(tag))\n" + @@ -1789,6 +1792,26 @@ private String generateDecodePrefix( " {\n"; } + private String emptyTagValidation() + { + if (allowEmptyTags) + { + return " else if (valueLength == 0 && position < (endOfField + 1))\n" + + " {\n" + + " position = endOfField + 1;\n" + + " continue;\n" + + " }\n"; + } + else + { + return " else if (valueLength == 0)\n" + + " {\n" + + " invalidTagId = tag;\n" + + " rejectReason = " + TAG_SPECIFIED_WITHOUT_A_VALUE + ";\n" + + " }\n"; + } + } + private String decodeTrailerOrReturn(final boolean hasCommonCompounds, final int indent) { return (hasCommonCompounds ? diff --git a/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/ExampleDictionary.java b/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/ExampleDictionary.java index 87d592f6c3..d492d9a1f8 100644 --- a/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/ExampleDictionary.java +++ b/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/ExampleDictionary.java @@ -347,6 +347,14 @@ public final class ExampleDictionary "8=FIX.4.4\0019=0027\00135=0\001115=abc\001116=2\001116=1\001117=1.1\001127=19700101-00:00:00.001" + "\00110=161\001"; + public static final String EMPTY_FIX_TAG_FOR_STRING_FIELD_MESSAGE = + "8=FIX.4.4\0019=81\00135=0\001115=abc\001112=\001116=2\001117=1.1" + + "\001118=Y\001200=3\001119=123\001127=19700101-00:00:00.001\00110=199\001"; + + public static final String EMPTY_FIX_TAG_FOR_LONG_FIELD_MESSAGE = + "8=FIX.4.4\0019=81\00135=0\001115=abc\001112=abc\001116=2\001117=1.1" + + "\001118=Y\001200=3\001119=123\001127=19700101-00:00:00.001\0011008=\00110=199\001"; + public static final String DERIVED_FIELDS_MESSAGE = "8=FIX.4.4\0019=53\00135=0\001115=abc\001116=2\001117=1.1\001127=19700101-00:00:00.001" + "\00110=043\001"; diff --git a/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/AbstractDecoderGeneratorTest.java b/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/AbstractDecoderGeneratorTest.java index a61ab81934..172d3ae6aa 100644 --- a/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/AbstractDecoderGeneratorTest.java +++ b/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/AbstractDecoderGeneratorTest.java @@ -72,6 +72,7 @@ public abstract class AbstractDecoderGeneratorTest private static Class heartbeatWithoutValidation; private static Class heartbeatWithoutEnumValueValidation; private static Class heartbeatWithRejectingUnknownFields; + private static Class heartbeatAllowingEmptyTags; private static Class heartbeat; private static Class component; private static Class otherMessage; @@ -85,13 +86,15 @@ public abstract class AbstractDecoderGeneratorTest static void generate(final boolean flyweightStringsEnabled) throws Exception { sourcesWithValidation = generateSources( - true, false, true, flyweightStringsEnabled, false); + true, false, true, flyweightStringsEnabled, false, false); final Map sourcesWithNoEnumValueValidation = generateSources( - true, false, false, flyweightStringsEnabled, false); + true, false, false, flyweightStringsEnabled, false, false); final Map sourcesWithoutValidation = generateSources( - false, false, true, flyweightStringsEnabled, true); + false, false, true, flyweightStringsEnabled, true, false); final Map sourcesRejectingUnknownFields = generateSources( - true, true, true, flyweightStringsEnabled, false); + true, true, true, flyweightStringsEnabled, false, false); + final Map sourcesAllowingEmptyTags = generateSources( + true, false, true, flyweightStringsEnabled, false, true); heartbeat = compileInMemory(HEARTBEAT_DECODER, sourcesWithValidation); if (heartbeat == null || CODEC_LOGGING) { @@ -109,6 +112,7 @@ static void generate(final boolean flyweightStringsEnabled) throws Exception heartbeatWithoutValidation = compileInMemory(HEARTBEAT_DECODER, sourcesWithoutValidation); heartbeatWithoutEnumValueValidation = compileInMemory(HEARTBEAT_DECODER, sourcesWithNoEnumValueValidation); heartbeatWithRejectingUnknownFields = compileInMemory(HEARTBEAT_DECODER, sourcesRejectingUnknownFields); + heartbeatAllowingEmptyTags = compileInMemory(HEARTBEAT_DECODER, sourcesAllowingEmptyTags); allReqFieldTypesMessage = compileInMemory(ALL_REQ_FIELD_TYPES_MESSAGE_DECODER, sourcesWithoutValidation); if (heartbeatWithoutValidation == null || CODEC_LOGGING) { @@ -118,7 +122,8 @@ static void generate(final boolean flyweightStringsEnabled) throws Exception private static Map generateSources( final boolean validation, final boolean rejectingUnknownFields, final boolean rejectingUnknownEnumValue, - final boolean flyweightStringsEnabled, final boolean wrapEmptyBuffer) + final boolean flyweightStringsEnabled, final boolean wrapEmptyBuffer, final boolean allowEmptyTags + ) { final Class validationClass = validation ? ValidationOn.class : ValidationOff.class; final Class rejectUnknownField = rejectingUnknownFields ? @@ -132,7 +137,7 @@ private static Map generateSources( final DecoderGenerator decoderGenerator = new DecoderGenerator( MESSAGE_EXAMPLE, 1, TEST_PACKAGE, TEST_PARENT_PACKAGE, TEST_PACKAGE, outputManager, validationClass, rejectUnknownField, - rejectUnknownEnumValue, flyweightStringsEnabled, wrapEmptyBuffer, + rejectUnknownEnumValue, flyweightStringsEnabled, wrapEmptyBuffer, allowEmptyTags, String.valueOf(rejectingUnknownEnumValue), true); final EncoderGenerator encoderGenerator = new EncoderGenerator(MESSAGE_EXAMPLE, TEST_PACKAGE, TEST_PARENT_PACKAGE, outputManager, ValidationOn.class, RejectUnknownFieldOn.class, @@ -967,6 +972,24 @@ public void shouldValidateTagNumbersDefinedForThisMessageWhenUnknownFieldPropIsS assertEquals("Wrong reject reason", TAG_NOT_DEFINED_FOR_THIS_MESSAGE_TYPE, decoder.rejectReason()); } + @Test + public void shouldAllowMessagesWithEmptyTagsForStringPropertyWhenAllowTagsPropIsSet() throws Exception + { + final Decoder decoder = decodeHeartbeatAllowingEmptyTags(EMPTY_FIX_TAG_FOR_STRING_FIELD_MESSAGE); + + assertTrue("Failed validation with empty fix tag", decoder.validate()); + assertFalse((Boolean)get(decoder, "hasTestReqID")); + } + + @Test + public void shouldAllowMessagesWithEmptyTagsForIntPropertyWhenAllowTagsPropIsSet() throws Exception + { + final Decoder decoder = decodeHeartbeatAllowingEmptyTags(EMPTY_FIX_TAG_FOR_LONG_FIELD_MESSAGE); + + assertTrue("Failed validation with empty fix tag", decoder.validate()); + assertFalse((Boolean)get(decoder, "hasLongField")); + } + @Test public void shouldSkipFieldUnknownToMessageButDefinedInFIXSpec() throws Exception { @@ -1927,6 +1950,13 @@ private Decoder decodeHeartbeatWithRejectingUnknownFields(final String example) return decoder; } + private Decoder decodeHeartbeatAllowingEmptyTags(final String example) throws Exception + { + final Decoder decoder = (Decoder)heartbeatAllowingEmptyTags.getConstructor().newInstance(); + decode(example, decoder); + return decoder; + } + void decode(final String example, final Decoder decoder) { buffer.putAscii(1, example); diff --git a/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/AcceptorGeneratorTest.java b/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/AcceptorGeneratorTest.java index 5c2c6e872c..fafa2bae8e 100644 --- a/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/AcceptorGeneratorTest.java +++ b/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/AcceptorGeneratorTest.java @@ -44,7 +44,7 @@ public class AcceptorGeneratorTest RejectUnknownFieldOn.class, RejectUnknownEnumValueOn.class, RUNTIME_REJECT_UNKNOWN_ENUM_VALUE_PROPERTY, true); private static final DecoderGenerator DECODER_GENERATOR = new DecoderGenerator( MESSAGE_EXAMPLE, 1, TEST_PACKAGE, TEST_PARENT_PACKAGE, TEST_PACKAGE, OUTPUT_MANAGER, ValidationOn.class, - RejectUnknownFieldOff.class, RejectUnknownEnumValueOn.class, false, false, + RejectUnknownFieldOff.class, RejectUnknownEnumValueOn.class, false, false, false, Generator.RUNTIME_REJECT_UNKNOWN_ENUM_VALUE_PROPERTY, true); private static final AcceptorGenerator ACCEPTOR_GENERATOR = new AcceptorGenerator( MESSAGE_EXAMPLE, TEST_PACKAGE, OUTPUT_MANAGER); diff --git a/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/CopyToEncoderGeneratorTest.java b/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/CopyToEncoderGeneratorTest.java index c89436301a..c7a55f781d 100644 --- a/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/CopyToEncoderGeneratorTest.java +++ b/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/CopyToEncoderGeneratorTest.java @@ -76,7 +76,7 @@ private static Map generateClasses() final DecoderGenerator decoderGenerator = new DecoderGenerator( MESSAGE_EXAMPLE, 1, TEST_PACKAGE, TEST_PARENT_PACKAGE, TEST_PACKAGE, outputManager, ValidationOn.class, - RejectUnknownFieldOn.class, RejectUnknownEnumValueOn.class, false, false, + RejectUnknownFieldOn.class, RejectUnknownEnumValueOn.class, false, false, false, RUNTIME_REJECT_UNKNOWN_ENUM_VALUE_PROPERTY, true); final EncoderGenerator encoderGenerator = new EncoderGenerator(MESSAGE_EXAMPLE, TEST_PACKAGE, TEST_PARENT_PACKAGE, outputManager, ValidationOn.class, RejectUnknownFieldOn.class, diff --git a/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/PrinterGeneratorTest.java b/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/PrinterGeneratorTest.java index 47d1409924..2f35fdb85b 100644 --- a/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/PrinterGeneratorTest.java +++ b/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/PrinterGeneratorTest.java @@ -41,7 +41,7 @@ public class PrinterGeneratorTest private static final DecoderGenerator DECODER_GENERATOR = new DecoderGenerator( MESSAGE_EXAMPLE, 1, TEST_PACKAGE, TEST_PARENT_PACKAGE, TEST_PACKAGE, OUTPUT_MANAGER, ValidationOn.class, - RejectUnknownFieldOff.class, RejectUnknownEnumValueOn.class, false, false, + RejectUnknownFieldOff.class, RejectUnknownEnumValueOn.class, false, false, false, Generator.RUNTIME_REJECT_UNKNOWN_ENUM_VALUE_PROPERTY, true); private static final EncoderGenerator ENCODER_GENERATOR = new EncoderGenerator( MESSAGE_EXAMPLE, TEST_PACKAGE, TEST_PARENT_PACKAGE, OUTPUT_MANAGER, ValidationOn.class, diff --git a/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/ToEncoderDecoderGeneratorTest.java b/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/ToEncoderDecoderGeneratorTest.java index ddb4516ad2..e35bb75174 100644 --- a/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/ToEncoderDecoderGeneratorTest.java +++ b/artio-codecs/src/test/java/uk/co/real_logic/artio/dictionary/generation/ToEncoderDecoderGeneratorTest.java @@ -93,7 +93,7 @@ private static Map generateClasses(final boolean flyweight final DecoderGenerator decoderGenerator = new DecoderGenerator( MESSAGE_EXAMPLE, 1, TEST_PACKAGE, TEST_PARENT_PACKAGE, TEST_PACKAGE, outputManager, ValidationOn.class, - RejectUnknownFieldOn.class, RejectUnknownEnumValueOn.class, flyweightStringsEnabled, false, + RejectUnknownFieldOn.class, RejectUnknownEnumValueOn.class, flyweightStringsEnabled, false, false, RUNTIME_REJECT_UNKNOWN_ENUM_VALUE_PROPERTY, true); final EncoderGenerator encoderGenerator = new EncoderGenerator(MESSAGE_EXAMPLE, TEST_PACKAGE, TEST_PARENT_PACKAGE, outputManager, ValidationOn.class, RejectUnknownFieldOn.class,