Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement: First field in repeating group as delimiter #674

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions quickfixj-base/src/main/java/quickfix/Message.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
Expand Down
17 changes: 17 additions & 0 deletions quickfixj-base/src/main/java/quickfix/ValidationSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class ValidationSettings {
boolean checkUserDefinedFields = true;
boolean checkUnorderedGroupFields = true;
boolean allowUnknownMessageFields = false;
boolean firstFieldInGroupIsDelimiter = false;

public ValidationSettings() {}

Expand All @@ -34,6 +35,7 @@ public ValidationSettings(ValidationSettings validationSettings) {
this.checkUserDefinedFields = validationSettings.checkUserDefinedFields;
this.checkUnorderedGroupFields = validationSettings.checkUnorderedGroupFields;
this.allowUnknownMessageFields = validationSettings.allowUnknownMessageFields;
this.firstFieldInGroupIsDelimiter = validationSettings.firstFieldInGroupIsDelimiter;
}

/**
Expand Down Expand Up @@ -65,6 +67,10 @@ public boolean isAllowUnknownMessageFields() {
return allowUnknownMessageFields;
}

public boolean isFirstFieldInGroupIsDelimiter() {
return firstFieldInGroupIsDelimiter;
}

/**
* Controls whether group fields are in the same order
*
Expand Down Expand Up @@ -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 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,14 @@ <H3>QuickFIX Settings</H3>
N</TD>
<TD>Y</TD>
</TR>
<TR ALIGN="left" VALIGN="middle">
<TD><I>FirstFieldInGroupIsDelimiter</I></TD>
<TD>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"</TD>
<TD>Y<br>
N</TD>
<TD>N</TD>
</TR>
<TR ALIGN="left" VALIGN="middle">
<TD><I>ValidateIncomingMessage</I></TD>
<TD>Allow to bypass the message validation (against the dictionary). Default is "Y".</TD>
Expand Down
17 changes: 17 additions & 0 deletions quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -37,6 +39,7 @@
* initiators) for creating sessions.
*/
public class DefaultSessionFactory implements SessionFactory {
private final static Logger log = LoggerFactory.getLogger(DefaultSessionFactory.class);
private static final SimpleCache<String, DataDictionary> dictionaryCache = new SimpleCache<>(path -> {
try {
return new DataDictionary(path);
Expand Down Expand Up @@ -314,9 +317,23 @@ private ValidationSettings createValidationSettings(SessionID sessionID, Session
Session.SETTING_ALLOW_UNKNOWN_MSG_FIELDS));
}

if (settings.isSetting(sessionID, Session.SETTING_FIRST_FIELD_IN_GROUP_IS_DELIMITER)) {
validationSettings.setFirstFieldInGroupIsDelimiter(settings.getBool(sessionID,
Session.SETTING_FIRST_FIELD_IN_GROUP_IS_DELIMITER));
}

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 {
Expand Down
6 changes: 6 additions & 0 deletions quickfixj-core/src/main/java/quickfix/Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down
64 changes: 64 additions & 0 deletions quickfixj-core/src/test/java/quickfix/MessageTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down
Loading