diff --git a/quickfixj-base/src/main/java/quickfix/Message.java b/quickfixj-base/src/main/java/quickfix/Message.java index ecdc9d649d..52874d43dd 100644 --- a/quickfixj-base/src/main/java/quickfix/Message.java +++ b/quickfixj-base/src/main/java/quickfix/Message.java @@ -733,7 +733,7 @@ private void parseGroup(String msgType, StringField field, DataDictionary dd, Da throw MessageUtils.newInvalidMessageException("Repeating group count requires an Integer but found '" + field.getValue() + "' in " + messageData, this); } parent.setField(groupCountTag, field); - final int firstField = rg.getDelimiterField(); + int firstField = dds.isFirstFieldInGroupIsDelimiter() ? -1 : rg.getDelimiterField(); Group group = null; boolean inGroupParse = true; while (inGroupParse) { @@ -743,7 +743,9 @@ private void parseGroup(String msgType, StringField field, DataDictionary dd, Da break; } int tag = field.getTag(); - if (tag == firstField) { + boolean shouldCreateNewGroup = tag == firstField || (dds.isFirstFieldInGroupIsDelimiter() && firstField == -1); + if (shouldCreateNewGroup) { + firstField = tag; addGroupRefToParent(group, parent); group = new Group(groupCountTag, firstField, groupDataDictionary.getOrderedFields()); group.setField(field); diff --git a/quickfixj-base/src/main/java/quickfix/ValidationSettings.java b/quickfixj-base/src/main/java/quickfix/ValidationSettings.java index f2d242eac6..b19332a630 100644 --- a/quickfixj-base/src/main/java/quickfix/ValidationSettings.java +++ b/quickfixj-base/src/main/java/quickfix/ValidationSettings.java @@ -25,6 +25,7 @@ public class ValidationSettings { boolean checkUserDefinedFields = true; boolean checkUnorderedGroupFields = true; boolean allowUnknownMessageFields = false; + boolean firstFieldInGroupIsDelimiter = false; public ValidationSettings() {} @@ -34,6 +35,7 @@ public ValidationSettings(ValidationSettings validationSettings) { this.checkUserDefinedFields = validationSettings.checkUserDefinedFields; this.checkUnorderedGroupFields = validationSettings.checkUnorderedGroupFields; this.allowUnknownMessageFields = validationSettings.allowUnknownMessageFields; + this.firstFieldInGroupIsDelimiter = validationSettings.firstFieldInGroupIsDelimiter; } /** @@ -65,6 +67,10 @@ public boolean isAllowUnknownMessageFields() { return allowUnknownMessageFields; } + public boolean isFirstFieldInGroupIsDelimiter() { + return firstFieldInGroupIsDelimiter; + } + /** * Controls whether group fields are in the same order * @@ -95,4 +101,15 @@ public void setCheckUserDefinedFields(boolean flag) { public void setAllowUnknownMessageFields(boolean allowUnknownFields) { allowUnknownMessageFields = allowUnknownFields; } + + /** + * Controls whether any field which is + * first in the repeating group would be used as delimiter + * + * @param flag true = use first field from message, false = follow data dictionary + * Must be used with disabled {@link #setCheckUnorderedGroupFields(boolean)} + */ + public void setFirstFieldInGroupIsDelimiter(boolean flag) { + firstFieldInGroupIsDelimiter = flag; + } } diff --git a/quickfixj-base/src/test/java/quickfix/ValidationSettingsTest.java b/quickfixj-base/src/test/java/quickfix/ValidationSettingsTest.java index a46eefff8d..6b50a8f5c4 100644 --- a/quickfixj-base/src/test/java/quickfix/ValidationSettingsTest.java +++ b/quickfixj-base/src/test/java/quickfix/ValidationSettingsTest.java @@ -14,6 +14,7 @@ public void copyConstructor_retains_settings() { validationSettings.setCheckFieldsOutOfOrder(false); validationSettings.setCheckUnorderedGroupFields(false); validationSettings.setCheckUserDefinedFields(false); + validationSettings.setFirstFieldInGroupIsDelimiter(true); ValidationSettings validationSettingsCopy = new ValidationSettings(validationSettings); @@ -22,5 +23,6 @@ public void copyConstructor_retains_settings() { assertEquals(validationSettingsCopy.isCheckFieldsOutOfOrder(), validationSettings.isCheckFieldsOutOfOrder()); assertEquals(validationSettingsCopy.isCheckUnorderedGroupFields(), validationSettings.isCheckUnorderedGroupFields()); assertEquals(validationSettingsCopy.isCheckUserDefinedFields(), validationSettings.isCheckUserDefinedFields()); + assertEquals(validationSettingsCopy.isFirstFieldInGroupIsDelimiter(), validationSettings.isFirstFieldInGroupIsDelimiter()); } } diff --git a/quickfixj-core/src/main/doc/usermanual/usage/configuration.html b/quickfixj-core/src/main/doc/usermanual/usage/configuration.html index 65a5c74178..6eeaec15b2 100644 --- a/quickfixj-core/src/main/doc/usermanual/usage/configuration.html +++ b/quickfixj-core/src/main/doc/usermanual/usage/configuration.html @@ -367,6 +367,14 @@

QuickFIX Settings

N Y + + FirstFieldInGroupIsDelimiter + Session validation setting for enabling whether first found field in repeating group will be used as + delimiter. Values are "Y" or "N". Default is "N". ValidateUnorderedGroupFields should be set to "N" + Y
+ N + N + ValidateIncomingMessage Allow to bypass the message validation (against the dictionary). Default is "Y". diff --git a/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java b/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java index 9da621fe7e..f131f5d4b2 100644 --- a/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java +++ b/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java @@ -21,6 +21,8 @@ import org.quickfixj.QFJException; import org.quickfixj.SimpleCache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import quickfix.field.ApplVerID; import quickfix.field.DefaultApplVerID; @@ -37,7 +39,8 @@ * initiators) for creating sessions. */ public class DefaultSessionFactory implements SessionFactory { - private static final SimpleCache dictionaryCache = new SimpleCache<>(path -> { + private static final Logger LOG = LoggerFactory.getLogger(DefaultSessionFactory.class); + private static final SimpleCache DICTIONARY_CACHE = new SimpleCache<>(path -> { try { return new DataDictionary(path); } catch (ConfigError e) { @@ -289,34 +292,36 @@ private DataDictionary createDataDictionary(SessionID sessionID, SessionSettings private ValidationSettings createValidationSettings(SessionID sessionID, SessionSettings settings) throws FieldConvertError, ConfigError { ValidationSettings validationSettings = new ValidationSettings(); - if (settings.isSetting(sessionID, Session.SETTING_VALIDATE_FIELDS_OUT_OF_ORDER)) { - validationSettings.setCheckFieldsOutOfOrder(settings.getBool(sessionID, - Session.SETTING_VALIDATE_FIELDS_OUT_OF_ORDER)); - } + validationSettings.setCheckFieldsOutOfOrder(settings.getBoolOrDefault(sessionID, + Session.SETTING_VALIDATE_FIELDS_OUT_OF_ORDER, validationSettings.isCheckFieldsOutOfOrder())); - if (settings.isSetting(sessionID, Session.SETTING_VALIDATE_FIELDS_HAVE_VALUES)) { - validationSettings.setCheckFieldsHaveValues(settings.getBool(sessionID, - Session.SETTING_VALIDATE_FIELDS_HAVE_VALUES)); - } + validationSettings.setCheckFieldsHaveValues(settings.getBoolOrDefault(sessionID, + Session.SETTING_VALIDATE_FIELDS_HAVE_VALUES, validationSettings.isCheckFieldsHaveValues())); - if (settings.isSetting(sessionID, Session.SETTING_VALIDATE_UNORDERED_GROUP_FIELDS)) { - validationSettings.setCheckUnorderedGroupFields(settings.getBool(sessionID, - Session.SETTING_VALIDATE_UNORDERED_GROUP_FIELDS)); - } + validationSettings.setCheckUnorderedGroupFields(settings.getBoolOrDefault(sessionID, + Session.SETTING_VALIDATE_UNORDERED_GROUP_FIELDS, validationSettings.isCheckUnorderedGroupFields())); - if (settings.isSetting(sessionID, Session.SETTING_VALIDATE_USER_DEFINED_FIELDS)) { - validationSettings.setCheckUserDefinedFields(settings.getBool(sessionID, - Session.SETTING_VALIDATE_USER_DEFINED_FIELDS)); - } + validationSettings.setCheckUserDefinedFields(settings.getBoolOrDefault(sessionID, + Session.SETTING_VALIDATE_USER_DEFINED_FIELDS, validationSettings.isCheckUserDefinedFields())); - if (settings.isSetting(sessionID, Session.SETTING_ALLOW_UNKNOWN_MSG_FIELDS)) { - validationSettings.setAllowUnknownMessageFields(settings.getBool(sessionID, - Session.SETTING_ALLOW_UNKNOWN_MSG_FIELDS)); - } + validationSettings.setAllowUnknownMessageFields(settings.getBoolOrDefault(sessionID, + Session.SETTING_ALLOW_UNKNOWN_MSG_FIELDS, validationSettings.isAllowUnknownMessageFields())); + + validationSettings.setFirstFieldInGroupIsDelimiter(settings.getBoolOrDefault(sessionID, + Session.SETTING_FIRST_FIELD_IN_GROUP_IS_DELIMITER, validationSettings.isFirstFieldInGroupIsDelimiter())); + + validateValidationSettings(validationSettings); return validationSettings; } + private void validateValidationSettings(ValidationSettings validationSettings) { + if (validationSettings.isFirstFieldInGroupIsDelimiter() && validationSettings.isCheckUnorderedGroupFields()) { + LOG.warn("Setting " + Session.SETTING_FIRST_FIELD_IN_GROUP_IS_DELIMITER + + " requires " + Session.SETTING_VALIDATE_UNORDERED_GROUP_FIELDS + " to be set to false"); + } + } + private void processFixtDataDictionaries(SessionID sessionID, SessionSettings settings, DefaultDataDictionaryProvider dataDictionaryProvider) throws ConfigError, FieldConvertError { @@ -384,7 +389,7 @@ private String toDictionaryPath(String beginString) { private DataDictionary getDataDictionary(String path) throws ConfigError { try { - return dictionaryCache.computeIfAbsent(path); + return DICTIONARY_CACHE.computeIfAbsent(path); } catch (QFJException e) { final Throwable cause = e.getCause(); if (cause instanceof ConfigError) { diff --git a/quickfixj-core/src/main/java/quickfix/Session.java b/quickfixj-core/src/main/java/quickfix/Session.java index 847f5c2885..f99b42d37a 100644 --- a/quickfixj-core/src/main/java/quickfix/Session.java +++ b/quickfixj-core/src/main/java/quickfix/Session.java @@ -208,6 +208,12 @@ public class Session implements Closeable { */ public static final String SETTING_VALIDATE_UNORDERED_GROUP_FIELDS = "ValidateUnorderedGroupFields"; + /** + * Session validation setting for enabling whether first found field in repeating group will be used as + * delimiter. Values are "Y" or "N". Default is "N". + */ + public static final String SETTING_FIRST_FIELD_IN_GROUP_IS_DELIMITER = "FirstFieldInGroupIsDelimiter"; + /** * Session validation setting for enabling whether field values are * validated. Empty fields values are not allowed. Values are "Y" or "N". diff --git a/quickfixj-core/src/test/java/quickfix/MessageTest.java b/quickfixj-core/src/test/java/quickfix/MessageTest.java index 9ae2ad0f52..5ee57911ff 100644 --- a/quickfixj-core/src/test/java/quickfix/MessageTest.java +++ b/quickfixj-core/src/test/java/quickfix/MessageTest.java @@ -27,6 +27,9 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; +import static quickfix.DataDictionaryTest.getDictionary; + import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.ZoneOffset; @@ -120,6 +123,7 @@ import quickfix.fix44.ExecutionReport; import quickfix.fix44.IndicationOfInterest; import quickfix.fix44.Logon; +import quickfix.fix44.NewOrderMultileg; import quickfix.fix44.Logon.NoMsgTypes; import quickfix.fix44.NewOrderCross; import quickfix.fix44.NewOrderSingle.NoPartyIDs; @@ -1445,6 +1449,66 @@ public void testValidateFieldsOutOfOrderPreFIXT11() throws Exception { assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); } + @Test + public void testFirstFieldInGroupIsDelimiter() throws Exception { + + final DataDictionary dataDictionary = new DataDictionary(getDictionary()); + ValidationSettings validationSettings = new ValidationSettings(); + + String fixMsg = "8=FIX.4.4\u00019=688\u000135=AB\u000149=AAA\u000156=BBB\u000134=21133\u000150=ABCABC" + + "\u000152=20230905-13:24:37.022\u000155=AAPL\u00011=ACC1\u000111=123456abcedf\u000121=1\u000138=5\u000154=1\u000140=2\u000144=-0.8" + + "\u000159=0\u000160=20230905-13:24:36.984\u0001100=ALGO\u0001167=MLEG\u0001555=3\u0001602=111\u0001600=AAA" + + "\u0001602=222\u0001654=231\u0001600=BBB\u0001602=333\u0001654=332\u0001600=CCC\u000158=TEXT\u000110=168\u0001"; + + String byDictFixMsg = "8=FIX.4.4\u00019=688\u000135=AB\u000149=AAA\u000156=BBB\u000134=21133\u000150=ABCABC" + + "\u000152=20230905-13:24:37.022\u000155=AAPL\u00011=ACC1\u000111=123456abcedf\u000121=1\u000138=5\u000154=1\u000140=2\u000144=-0.8" + + "\u000159=0\u000160=20230905-13:24:36.984\u0001100=ALGO\u0001167=MLEG\u0001555=3\u0001600=AAA\u0001602=111" + + "\u0001600=BBB\u0001602=222\u0001654=231\u0001600=CCC\u0001602=333\u0001654=332\u000158=TEXT\u000110=168\u0001"; + + validationSettings.setFirstFieldInGroupIsDelimiter(true); + validationSettings.setCheckUnorderedGroupFields(false); + final NewOrderMultileg noml1 = new NewOrderMultileg(); + noml1.fromString(fixMsg, dataDictionary, validationSettings, true); + dataDictionary.validate(noml1, validationSettings); + assertTrue(noml1.hasGroup(555)); + assertEquals(3, noml1.getGroupCount(555)); + //when firstFieldInGroupIsDelimiter = true and setCheckUnorderedGroupFields = false - valid + //delimiter should be first tag in group + assertEquals(602, noml1.getGroup(1, 555).delim()); + + validationSettings.setFirstFieldInGroupIsDelimiter(false); + validationSettings.setCheckUnorderedGroupFields(false); + final NewOrderMultileg noml2 = new NewOrderMultileg(); + noml2.fromString(fixMsg, dataDictionary, validationSettings, true); + //when firstFieldInGroupIsDelimiter = false and setCheckUnorderedGroupFields = false - exception is thrown + assertThrows(FieldException.class, () -> dataDictionary.validate(noml2, validationSettings)); + + validationSettings.setFirstFieldInGroupIsDelimiter(false); + validationSettings.setCheckUnorderedGroupFields(true); + final NewOrderMultileg noml3 = new NewOrderMultileg(); + noml3.fromString(fixMsg, dataDictionary, validationSettings, true); + //when firstFieldInGroupIsDelimiter = false and setCheckUnorderedGroupFields = true - exception is thrown + assertThrows(FieldException.class, () -> dataDictionary.validate(noml3, validationSettings)); + + validationSettings.setFirstFieldInGroupIsDelimiter(true); + validationSettings.setCheckUnorderedGroupFields(true); + final NewOrderMultileg noml4 = new NewOrderMultileg(); + noml4.fromString(fixMsg, dataDictionary, validationSettings, true); + //when firstFieldInGroupIsDelimiter = true and setCheckUnorderedGroupFields = true - exception is thrown, since order of tags is incorrect. + assertThrows(FieldException.class, () -> dataDictionary.validate(noml4, validationSettings)); + + validationSettings.setFirstFieldInGroupIsDelimiter(true); + validationSettings.setCheckUnorderedGroupFields(true); + final NewOrderMultileg noml5 = new NewOrderMultileg(); + noml5.fromString(byDictFixMsg, dataDictionary, validationSettings, true); + //when firstFieldInGroupIsDelimiter = true and setCheckUnorderedGroupFields = true, message aligns with dictionary - do NOT fail + dataDictionary.validate(noml5, validationSettings); + assertTrue(noml5.hasGroup(555)); + assertEquals(3, noml5.getGroupCount(555)); + //delimiter should be dictionary first tag = 600 + assertEquals(600, noml5.getGroup(1, 555).delim()); + } + private void assertHeaderField(Message message, String expectedValue, int field) throws FieldNotFound { assertEquals(expectedValue, message.getHeader().getString(field));