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));