From 241b97a7d5971d92cd7b1bea914d0f1c8c8bc4fc Mon Sep 17 00:00:00 2001 From: konradbloor Date: Sat, 15 Aug 2026 10:31:20 +0100 Subject: [PATCH 1/3] Extract validation logic from `DataDictionary` into `DataDictionaryValidator` `DataDictionaryValidator` holds the `ValidationSettings` as instance state, so the validation methods no longer need to pass settings (or individual flags) as parameters. The public `DataDictionary.validate()` overloads are kept and delegate to the new class, so no API change for callers. Method bodies are moved verbatim apart from referencing the dictionary and settings through `dd.` and `settings.`. --- .../main/java/quickfix/DataDictionary.java | 237 +------------ .../quickfix/DataDictionaryValidator.java | 319 ++++++++++++++++++ .../quickfix/DataDictionaryValidatorTest.java | 314 +++++++++++++++++ 3 files changed, 642 insertions(+), 228 deletions(-) create mode 100644 quickfixj-base/src/main/java/quickfix/DataDictionaryValidator.java create mode 100644 quickfixj-base/src/test/java/quickfix/DataDictionaryValidatorTest.java diff --git a/quickfixj-base/src/main/java/quickfix/DataDictionary.java b/quickfixj-base/src/main/java/quickfix/DataDictionary.java index f7345a38f..cafe3e487 100644 --- a/quickfixj-base/src/main/java/quickfix/DataDictionary.java +++ b/quickfixj-base/src/main/java/quickfix/DataDictionary.java @@ -24,17 +24,9 @@ import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; -import quickfix.field.BeginString; import quickfix.field.MsgType; import quickfix.field.SessionRejectReason; -import quickfix.field.converter.BooleanConverter; -import quickfix.field.converter.CharArrayConverter; -import quickfix.field.converter.CharConverter; -import quickfix.field.converter.DoubleConverter; import quickfix.field.converter.IntConverter; -import quickfix.field.converter.UtcDateOnlyConverter; -import quickfix.field.converter.UtcTimeOnlyConverter; -import quickfix.field.converter.UtcTimestampConverter; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -603,7 +595,7 @@ private static void copyCollection(Collection lhs, Collection rhs) { */ public void validate(Message message, ValidationSettings settings) throws IncorrectTagValue, FieldNotFound, IncorrectDataFormat { - validate(message, false, settings); + new DataDictionaryValidator(settings).validate(this, message); } /** @@ -618,80 +610,23 @@ public void validate(Message message, ValidationSettings settings) throws Incorr */ public void validate(Message message, boolean bodyOnly, ValidationSettings settings) throws IncorrectTagValue, FieldNotFound, IncorrectDataFormat { - validate(message, bodyOnly ? null : this, this, settings); + new DataDictionaryValidator(settings).validate(this, message, bodyOnly); } static void validate(Message message, DataDictionary sessionDataDictionary, DataDictionary applicationDataDictionary, ValidationSettings settings) throws IncorrectTagValue, FieldNotFound, IncorrectDataFormat { - final boolean bodyOnly = sessionDataDictionary == null; - if (settings == null) { - settings = new ValidationSettings(); - } - - if (isVersionSpecified(sessionDataDictionary) - && !sessionDataDictionary.getVersion().equals( - message.getHeader().getString(BeginString.FIELD)) - && !message.getHeader().getString(BeginString.FIELD).equals("FIXT.1.1") - && !sessionDataDictionary.getVersion().equals("FIX.5.0")) { - throw new UnsupportedVersion("Message version '" + message.getHeader().getString(BeginString.FIELD) - + "' does not match the data dictionary version '" + sessionDataDictionary.getVersion() + "'"); - } - - if (!message.hasValidStructure() && message.getException() != null) { - throw message.getException(); - } - - final String msgType = message.getHeader().getString(MsgType.FIELD); - if (isVersionSpecified(applicationDataDictionary)) { - applicationDataDictionary.checkMsgType(msgType); - applicationDataDictionary.checkHasRequired(message.getHeader(), message, - message.getTrailer(), msgType, bodyOnly); - } - - if (!bodyOnly) { - sessionDataDictionary.iterate(settings, message.getHeader(), HEADER_ID, sessionDataDictionary); - sessionDataDictionary.iterate(settings, message.getTrailer(), TRAILER_ID, sessionDataDictionary); - } - - applicationDataDictionary.iterate(settings, message, msgType, applicationDataDictionary); - } - - private static boolean isVersionSpecified(DataDictionary dd) { - return dd != null && dd.hasVersion; + new DataDictionaryValidator(settings).validate(message, sessionDataDictionary, applicationDataDictionary); } - private void iterate(ValidationSettings settings, FieldMap map, String msgType, DataDictionary dd) throws IncorrectTagValue, - IncorrectDataFormat { - for (final Field f : map) { - final StringField field = (StringField) f; - - checkHasValue(settings, field); - - if (hasVersion) { - checkValidFormat(settings, field); - checkValue(settings.allowUnknownEnumValues, field); - } - - if (beginString != null) { - dd.checkField(settings, field, msgType, map instanceof Message); - dd.checkGroupCount(field, map, msgType); - } - } - - for (final List groups : map.getGroups().values()) { - for (final Group group : groups) { - iterate(settings, group, msgType, dd.getGroup(msgType, group.getFieldTag()) - .getDataDictionary()); - } - } + /** Check if this dictionary was loaded with a FIX version. **/ + boolean hasVersion() { + return hasVersion; } - /** Check if message type is defined in spec. **/ - private void checkMsgType(String msgType) { - if (!isMsgType(msgType)) { - throw new FieldException(SessionRejectReason.INVALID_MSGTYPE, MsgType.FIELD); - } + /** Get the required fields for a message type, or null if there are none. **/ + Set getRequiredFields(String msgType) { + return requiredFields.get(msgType); } /** Check if field tag number is defined in spec. **/ @@ -701,21 +636,6 @@ void checkValidTagNumber(Field field) { } } - /** Check if field tag is defined for message or group **/ - void checkField(ValidationSettings settings, Field field, String msgType, boolean message) { - // use different validation for groups and messages - boolean messageField = message ? isMsgField(msgType, field.getField()) : fields.contains(field.getField()); - boolean fail = checkFieldFailure(settings, field.getField(), messageField); - - if (fail) { - if (fields.contains(field.getField())) { - throw new FieldException(SessionRejectReason.TAG_NOT_DEFINED_FOR_THIS_MESSAGE_TYPE, field.getField()); - } else { - throw new FieldException(SessionRejectReason.INVALID_TAG_NUMBER, field.getField()); - } - } - } - boolean checkFieldFailure(ValidationSettings settings, int field, boolean messageField) { boolean fail; if (field < USER_DEFINED_TAG_MIN) { @@ -726,145 +646,6 @@ boolean checkFieldFailure(ValidationSettings settings, int field, boolean messag return fail; } - private void checkValidFormat(ValidationSettings settings, StringField field) throws IncorrectDataFormat { - FieldType fieldType = getFieldType(field.getTag()); - if (fieldType == null) { - return; - } - if (!settings.checkFieldsHaveValues && field.getValue().length() == 0) { - return; - } - try { - switch (fieldType) { - case STRING: - case MULTIPLEVALUESTRING: - case MULTIPLESTRINGVALUE: - case EXCHANGE: - case LOCALMKTDATE: - case DATA: - case MONTHYEAR: - case DAYOFMONTH: - case COUNTRY: - // String - break; - case MULTIPLECHARVALUE: - CharArrayConverter.convert(field.getValue()); - break; - case INT: - case NUMINGROUP: - case SEQNUM: - case LENGTH: - IntConverter.convert(field.getValue()); - break; - case PRICE: - case AMT: - case QTY: - case FLOAT: - case PRICEOFFSET: - case PERCENTAGE: - DoubleConverter.convert(field.getValue()); - break; - case BOOLEAN: - BooleanConverter.convert(field.getValue()); - break; - case UTCDATE: - UtcDateOnlyConverter.convert(field.getValue()); - break; - case UTCTIMEONLY: - UtcTimeOnlyConverter.convert(field.getValue()); - break; - case UTCTIMESTAMP: - case TIME: - UtcTimestampConverter.convert(field.getValue()); - break; - case CHAR: - if (beginString.compareTo(FixVersions.BEGINSTRING_FIX41) > 0) { - CharConverter.convert(field.getValue()); - } // otherwise it's a String, for older FIX versions - break; - } - } catch (final FieldConvertError e) { - throw new IncorrectDataFormat(field.getTag(), field.getValue()); - } - } - - private void checkValue(boolean allowUnknownEnumValues, StringField field) throws IncorrectTagValue { - if (allowUnknownEnumValues) { - return; - } - int tag = field.getField(); - if (hasFieldValue(tag) && !isFieldValue(tag, field.getValue())) { - throw new IncorrectTagValue(tag); - } - } - - /** Check if a field has a value. **/ - private void checkHasValue(ValidationSettings settings, StringField field) { - if (settings.checkFieldsHaveValues && field.getValue().length() == 0) { - throw new FieldException(SessionRejectReason.TAG_SPECIFIED_WITHOUT_A_VALUE, - field.getField()); - } - } - - /** - * Check if group count matches number of groups in message. * - */ - private void checkGroupCount(StringField field, FieldMap fieldMap, String msgType) { - final int fieldNum = field.getField(); - if (isGroup(msgType, fieldNum)) { - try { - if (fieldMap.getGroupCount(fieldNum) != IntConverter.convert(field.getValue())) { - throwNewFieldException(fieldNum); - } - } catch (FieldConvertError ex) { - throwNewFieldException(fieldNum); - } - } - } - - private void throwNewFieldException(final int fieldNum) throws FieldException { - throw new FieldException( - SessionRejectReason.INCORRECT_NUMINGROUP_COUNT_FOR_REPEATING_GROUP, - fieldNum); - } - - /** Check if a message has all required fields. **/ - void checkHasRequired(FieldMap header, FieldMap body, FieldMap trailer, String msgType, - boolean bodyOnly) { - if (!bodyOnly) { - checkHasRequired(HEADER_ID, header, bodyOnly); - checkHasRequired(TRAILER_ID, trailer, bodyOnly); - } - - checkHasRequired(msgType, body, bodyOnly); - } - - private void checkHasRequired(String msgType, FieldMap fields, boolean bodyOnly) { - final Set requiredFieldsForMessage = requiredFields.get(msgType); - if (requiredFieldsForMessage == null || requiredFieldsForMessage.isEmpty()) { - return; - } - - for (int field : requiredFieldsForMessage) { - if (!fields.isSetField(field)) { - throw new FieldException(SessionRejectReason.REQUIRED_TAG_MISSING, field); - } - } - - final Map> groups = fields.getGroups(); - if (!groups.isEmpty()) { - for (Map.Entry> entry : groups.entrySet()) { - final GroupInfo p = getGroup(msgType, entry.getKey()); - if (p != null) { - for (Group groupInstance : entry.getValue()) { - p.getDataDictionary().checkHasRequired(groupInstance, groupInstance, - groupInstance, msgType, bodyOnly); - } - } - } - } - } - private int countElementNodes(NodeList nodes) { int elementNodesCount = 0; diff --git a/quickfixj-base/src/main/java/quickfix/DataDictionaryValidator.java b/quickfixj-base/src/main/java/quickfix/DataDictionaryValidator.java new file mode 100644 index 000000000..048aec076 --- /dev/null +++ b/quickfixj-base/src/main/java/quickfix/DataDictionaryValidator.java @@ -0,0 +1,319 @@ +/******************************************************************************* + * Copyright (c) quickfixengine.org All rights reserved. + * + * This file is part of the QuickFIX FIX Engine + * + * This file may be distributed under the terms of the quickfixengine.org + * license as defined by quickfixengine.org and appearing in the file + * LICENSE included in the packaging of this file. + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING + * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE. + * + * See http://www.quickfixengine.org/LICENSE for licensing information. + * + * Contact ask@quickfixengine.org if any conditions of this licensing + * are not clear to you. + ******************************************************************************/ + +package quickfix; + +import quickfix.field.BeginString; +import quickfix.field.MsgType; +import quickfix.field.SessionRejectReason; +import quickfix.field.converter.BooleanConverter; +import quickfix.field.converter.CharArrayConverter; +import quickfix.field.converter.CharConverter; +import quickfix.field.converter.DoubleConverter; +import quickfix.field.converter.IntConverter; +import quickfix.field.converter.UtcDateOnlyConverter; +import quickfix.field.converter.UtcTimeOnlyConverter; +import quickfix.field.converter.UtcTimestampConverter; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Validates messages against the metadata of a {@link DataDictionary}, + * applying the given {@link ValidationSettings}. + */ +public class DataDictionaryValidator { + + private final ValidationSettings settings; + + /** + * @param settings the validation settings to apply; may be null, in which + * case default settings are used + */ + public DataDictionaryValidator(ValidationSettings settings) { + this.settings = settings != null ? settings : new ValidationSettings(); + } + + /** + * Validate a message, including the header and trailer fields. + * + * @param dataDictionary the dictionary to validate against + * @param message the message + * @throws IncorrectTagValue if a field value is not valid + * @throws FieldNotFound if a field cannot be found + * @throws IncorrectDataFormat if a field value has a wrong data type + */ + public void validate(DataDictionary dataDictionary, Message message) throws IncorrectTagValue, + FieldNotFound, IncorrectDataFormat { + validate(dataDictionary, message, false); + } + + /** + * Validate the message body, with header and trailer fields being validated conditionally. + * + * @param dataDictionary the dictionary to validate against + * @param message the message + * @param bodyOnly whether to validate just the message body, or to validate the header and trailer sections as well. + * @throws IncorrectTagValue if a field value is not valid + * @throws FieldNotFound if a field cannot be found + * @throws IncorrectDataFormat if a field value has a wrong data type + */ + public void validate(DataDictionary dataDictionary, Message message, boolean bodyOnly) + throws IncorrectTagValue, FieldNotFound, IncorrectDataFormat { + validate(message, bodyOnly ? null : dataDictionary, dataDictionary); + } + + /** + * Validate the message header and trailer against the session data dictionary + * and the body against the application data dictionary. + * + * @param message the message + * @param sessionDataDictionary the dictionary for the header and trailer; if null, only the body is validated + * @param applicationDataDictionary the dictionary for the message body + * @throws IncorrectTagValue if a field value is not valid + * @throws FieldNotFound if a field cannot be found + * @throws IncorrectDataFormat if a field value has a wrong data type + */ + public void validate(Message message, DataDictionary sessionDataDictionary, + DataDictionary applicationDataDictionary) throws IncorrectTagValue, FieldNotFound, + IncorrectDataFormat { + final boolean bodyOnly = sessionDataDictionary == null; + + if (isVersionSpecified(sessionDataDictionary) + && !sessionDataDictionary.getVersion().equals( + message.getHeader().getString(BeginString.FIELD)) + && !message.getHeader().getString(BeginString.FIELD).equals("FIXT.1.1") + && !sessionDataDictionary.getVersion().equals("FIX.5.0")) { + throw new UnsupportedVersion("Message version '" + message.getHeader().getString(BeginString.FIELD) + + "' does not match the data dictionary version '" + sessionDataDictionary.getVersion() + "'"); + } + + if (!message.hasValidStructure() && message.getException() != null) { + throw message.getException(); + } + + final String msgType = message.getHeader().getString(MsgType.FIELD); + if (isVersionSpecified(applicationDataDictionary)) { + checkMsgType(applicationDataDictionary, msgType); + checkHasRequired(applicationDataDictionary, message.getHeader(), message, + message.getTrailer(), msgType, bodyOnly); + } + + if (!bodyOnly) { + iterate(sessionDataDictionary, message.getHeader(), DataDictionary.HEADER_ID, + sessionDataDictionary); + iterate(sessionDataDictionary, message.getTrailer(), DataDictionary.TRAILER_ID, + sessionDataDictionary); + } + + iterate(applicationDataDictionary, message, msgType, applicationDataDictionary); + } + + private static boolean isVersionSpecified(DataDictionary dd) { + return dd != null && dd.hasVersion(); + } + + private void iterate(DataDictionary rootDictionary, FieldMap map, String msgType, + DataDictionary dd) throws IncorrectTagValue, IncorrectDataFormat { + for (final Field f : map) { + final StringField field = (StringField) f; + + checkHasValue(field); + + if (rootDictionary.hasVersion()) { + checkValidFormat(rootDictionary, field); + checkValue(rootDictionary, field); + } + + if (rootDictionary.getVersion() != null) { + checkField(dd, field, msgType, map instanceof Message); + checkGroupCount(dd, field, map, msgType); + } + } + + for (final List groups : map.getGroups().values()) { + for (final Group group : groups) { + iterate(rootDictionary, group, msgType, dd.getGroup(msgType, group.getFieldTag()) + .getDataDictionary()); + } + } + } + + /** Check if message type is defined in spec. **/ + private void checkMsgType(DataDictionary dd, String msgType) { + if (!dd.isMsgType(msgType)) { + throw new FieldException(SessionRejectReason.INVALID_MSGTYPE, MsgType.FIELD); + } + } + + /** Check if field tag is defined for message or group **/ + private void checkField(DataDictionary dd, Field field, String msgType, boolean message) { + // use different validation for groups and messages + boolean messageField = message ? dd.isMsgField(msgType, field.getField()) : dd.isField(field.getField()); + boolean fail = dd.checkFieldFailure(settings, field.getField(), messageField); + + if (fail) { + if (dd.isField(field.getField())) { + throw new FieldException(SessionRejectReason.TAG_NOT_DEFINED_FOR_THIS_MESSAGE_TYPE, field.getField()); + } else { + throw new FieldException(SessionRejectReason.INVALID_TAG_NUMBER, field.getField()); + } + } + } + + private void checkValidFormat(DataDictionary dd, StringField field) throws IncorrectDataFormat { + FieldType fieldType = dd.getFieldType(field.getTag()); + if (fieldType == null) { + return; + } + if (!settings.checkFieldsHaveValues && field.getValue().length() == 0) { + return; + } + try { + switch (fieldType) { + case STRING: + case MULTIPLEVALUESTRING: + case MULTIPLESTRINGVALUE: + case EXCHANGE: + case LOCALMKTDATE: + case DATA: + case MONTHYEAR: + case DAYOFMONTH: + case COUNTRY: + // String + break; + case MULTIPLECHARVALUE: + CharArrayConverter.convert(field.getValue()); + break; + case INT: + case NUMINGROUP: + case SEQNUM: + case LENGTH: + IntConverter.convert(field.getValue()); + break; + case PRICE: + case AMT: + case QTY: + case FLOAT: + case PRICEOFFSET: + case PERCENTAGE: + DoubleConverter.convert(field.getValue()); + break; + case BOOLEAN: + BooleanConverter.convert(field.getValue()); + break; + case UTCDATE: + UtcDateOnlyConverter.convert(field.getValue()); + break; + case UTCTIMEONLY: + UtcTimeOnlyConverter.convert(field.getValue()); + break; + case UTCTIMESTAMP: + case TIME: + UtcTimestampConverter.convert(field.getValue()); + break; + case CHAR: + if (dd.getVersion().compareTo(FixVersions.BEGINSTRING_FIX41) > 0) { + CharConverter.convert(field.getValue()); + } // otherwise it's a String, for older FIX versions + break; + } + } catch (final FieldConvertError e) { + throw new IncorrectDataFormat(field.getTag(), field.getValue()); + } + } + + private void checkValue(DataDictionary dd, StringField field) throws IncorrectTagValue { + if (settings.allowUnknownEnumValues) { + return; + } + int tag = field.getField(); + if (dd.hasFieldValue(tag) && !dd.isFieldValue(tag, field.getValue())) { + throw new IncorrectTagValue(tag); + } + } + + /** Check if a field has a value. **/ + private void checkHasValue(StringField field) { + if (settings.checkFieldsHaveValues && field.getValue().length() == 0) { + throw new FieldException(SessionRejectReason.TAG_SPECIFIED_WITHOUT_A_VALUE, + field.getField()); + } + } + + /** + * Check if group count matches number of groups in message. * + */ + private void checkGroupCount(DataDictionary dd, StringField field, FieldMap fieldMap, String msgType) { + final int fieldNum = field.getField(); + if (dd.isGroup(msgType, fieldNum)) { + try { + if (fieldMap.getGroupCount(fieldNum) != IntConverter.convert(field.getValue())) { + throwNewFieldException(fieldNum); + } + } catch (FieldConvertError ex) { + throwNewFieldException(fieldNum); + } + } + } + + private void throwNewFieldException(final int fieldNum) throws FieldException { + throw new FieldException( + SessionRejectReason.INCORRECT_NUMINGROUP_COUNT_FOR_REPEATING_GROUP, + fieldNum); + } + + /** Check if a message has all required fields. **/ + private void checkHasRequired(DataDictionary dd, FieldMap header, FieldMap body, FieldMap trailer, + String msgType, boolean bodyOnly) { + if (!bodyOnly) { + checkHasRequired(dd, DataDictionary.HEADER_ID, header, bodyOnly); + checkHasRequired(dd, DataDictionary.TRAILER_ID, trailer, bodyOnly); + } + + checkHasRequired(dd, msgType, body, bodyOnly); + } + + private void checkHasRequired(DataDictionary dd, String msgType, FieldMap fields, boolean bodyOnly) { + final Set requiredFieldsForMessage = dd.getRequiredFields(msgType); + if (requiredFieldsForMessage == null || requiredFieldsForMessage.isEmpty()) { + return; + } + + for (int field : requiredFieldsForMessage) { + if (!fields.isSetField(field)) { + throw new FieldException(SessionRejectReason.REQUIRED_TAG_MISSING, field); + } + } + + final Map> groups = fields.getGroups(); + if (!groups.isEmpty()) { + for (Map.Entry> entry : groups.entrySet()) { + final DataDictionary.GroupInfo p = dd.getGroup(msgType, entry.getKey()); + if (p != null) { + for (Group groupInstance : entry.getValue()) { + checkHasRequired(p.getDataDictionary(), groupInstance, groupInstance, + groupInstance, msgType, bodyOnly); + } + } + } + } + } +} diff --git a/quickfixj-base/src/test/java/quickfix/DataDictionaryValidatorTest.java b/quickfixj-base/src/test/java/quickfix/DataDictionaryValidatorTest.java new file mode 100644 index 000000000..00a1360f4 --- /dev/null +++ b/quickfixj-base/src/test/java/quickfix/DataDictionaryValidatorTest.java @@ -0,0 +1,314 @@ +/******************************************************************************* + * Copyright (c) quickfixengine.org All rights reserved. + * + * This file is part of the QuickFIX FIX Engine + * + * This file may be distributed under the terms of the quickfixengine.org + * license as defined by quickfixengine.org and appearing in the file + * LICENSE included in the packaging of this file. + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING + * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE. + * + * See http://www.quickfixengine.org/LICENSE for licensing information. + * + * Contact ask@quickfixengine.org if any conditions of this licensing + * are not clear to you. + ******************************************************************************/ + +package quickfix; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; + +import java.io.ByteArrayInputStream; + +import org.junit.Test; + +import quickfix.field.SessionRejectReason; + +public class DataDictionaryValidatorTest { + + private static final String VALID_ORDER = + "8=FIX.4.4\0019=136\00135=D\00134=25\00149=SENDER\00156=TARGET\00152=20110412-13:43:00\001" + + "60=20110412-13:43:00\0011=testAccount\00111=123\00121=3\00138=42\00140=2\00144=42.37\001" + + "54=1\00155=QFJ\00159=0\00110=239\001"; + + // TimeInForce(59)=Z is not a valid enum value in FIX 4.4 + private static final String ORDER_WITH_UNKNOWN_ENUM_VALUE = + "8=FIX.4.4\0019=136\00135=D\00134=25\00149=SENDER\00156=TARGET\00152=20110412-13:43:00\001" + + "60=20110412-13:43:00\0011=testAccount\00111=123\00121=3\00138=42\00140=2\00144=42.37\001" + + "54=1\00155=QFJ\00159=Z\00110=239\001"; + + // missing required header field SendingTime(52) + private static final String ORDER_WITHOUT_SENDING_TIME = + "8=FIX.4.4\0019=113\00135=D\00134=25\00149=SENDER\00156=TARGET\001" + + "60=20110412-13:43:00\0011=testAccount\00111=123\00121=3\00138=42\00140=2\00144=42.37\001" + + "54=1\00155=QFJ\00159=0\00110=084\001"; + + private static final String FIX42_ORDER = + "8=FIX.4.2\0019=136\00135=D\00134=25\00149=SENDER\00156=TARGET\00152=20110412-13:43:00\001" + + "60=20110412-13:43:00\0011=testAccount\00111=123\00121=3\00138=42\00140=2\00144=42.37\001" + + "54=1\00155=QFJ\00159=0\00110=239\001"; + + @Test + public void testValidMessageIsAccepted() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + Message message = parse(dictionary, VALID_ORDER); + + new DataDictionaryValidator(new ValidationSettings()).validate(dictionary, message); + } + + @Test + public void testNullSettingsFallBackToDefaults() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + Message message = parse(dictionary, ORDER_WITH_UNKNOWN_ENUM_VALUE); + DataDictionaryValidator validator = new DataDictionaryValidator(null); + + assertThrows(IncorrectTagValue.class, () -> validator.validate(dictionary, message)); + } + + @Test + public void testSettingsAreApplied() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + Message message = parse(dictionary, ORDER_WITH_UNKNOWN_ENUM_VALUE); + ValidationSettings settings = new ValidationSettings(); + settings.setAllowUnknownEnumValues(true); + + new DataDictionaryValidator(settings).validate(dictionary, message); + } + + @Test + public void testBodyOnlySkipsHeaderValidation() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + Message message = parse(dictionary, ORDER_WITHOUT_SENDING_TIME); + DataDictionaryValidator validator = new DataDictionaryValidator(new ValidationSettings()); + + assertThrows(FieldException.class, () -> validator.validate(dictionary, message, false)); + + validator.validate(dictionary, message, true); + } + + @Test + public void testVersionMismatchIsRejected() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + Message message = parse(dictionary, FIX42_ORDER); + DataDictionaryValidator validator = new DataDictionaryValidator(new ValidationSettings()); + + assertThrows(UnsupportedVersion.class, + () -> validator.validate(message, dictionary, dictionary)); + } + + @Test + public void testBodyOnlyOverloadOnDataDictionary() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + Message message = parse(dictionary, ORDER_WITHOUT_SENDING_TIME); + + dictionary.validate(message, true, new ValidationSettings()); + } + + @Test + public void testUnknownMsgTypeIsRejected() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + Message message = parse(dictionary, VALID_ORDER.replace("35=D\001", "35=ZZ\001")); + DataDictionaryValidator validator = new DataDictionaryValidator(new ValidationSettings()); + + FieldException e = assertThrows(FieldException.class, + () -> validator.validate(dictionary, message)); + assertEquals(SessionRejectReason.INVALID_MSGTYPE, e.getSessionRejectReason()); + } + + @Test + public void testFieldNotDefinedForMessageIsRejected() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + // MDEntryDate(272) is defined in FIX 4.4 but not for NewOrderSingle + Message message = parse(dictionary, VALID_ORDER.replace("55=QFJ\001", "55=QFJ\001272=20260819\001")); + DataDictionaryValidator validator = new DataDictionaryValidator(new ValidationSettings()); + + FieldException e = assertThrows(FieldException.class, + () -> validator.validate(dictionary, message)); + assertEquals(SessionRejectReason.TAG_NOT_DEFINED_FOR_THIS_MESSAGE_TYPE, e.getSessionRejectReason()); + } + + @Test + public void testUnknownTagIsRejected() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + Message message = parse(dictionary, VALID_ORDER.replace("55=QFJ\001", "55=QFJ\001999=X\001")); + DataDictionaryValidator validator = new DataDictionaryValidator(new ValidationSettings()); + + FieldException e = assertThrows(FieldException.class, + () -> validator.validate(dictionary, message)); + assertEquals(SessionRejectReason.INVALID_TAG_NUMBER, e.getSessionRejectReason()); + } + + @Test + public void testUnknownTagIsAcceptedWhenUnknownMessageFieldsAllowed() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + Message message = parse(dictionary, VALID_ORDER.replace("55=QFJ\001", "55=QFJ\001999=X\001")); + ValidationSettings settings = new ValidationSettings(); + settings.setAllowUnknownMessageFields(true); + + new DataDictionaryValidator(settings).validate(dictionary, message); + } + + @Test + public void testInvalidTimestampFormatIsRejected() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + Message message = parse(dictionary, + VALID_ORDER.replace("60=20110412-13:43:00\001", "60=notatimestamp\001")); + DataDictionaryValidator validator = new DataDictionaryValidator(new ValidationSettings()); + + assertThrows(IncorrectDataFormat.class, () -> validator.validate(dictionary, message)); + } + + @Test + public void testBooleanAndDateFieldFormatsAreAccepted() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + // LocateReqd(114) BOOLEAN, MDEntryDate(272) UTCDATEONLY, MDEntryTime(273) UTCTIMEONLY + Message message = parse(dictionary, VALID_ORDER.replace("55=QFJ\001", + "55=QFJ\001114=Y\001272=20260819\001273=13:43:00\001")); + ValidationSettings settings = new ValidationSettings(); + settings.setAllowUnknownMessageFields(true); + + new DataDictionaryValidator(settings).validate(dictionary, message); + } + + @Test + public void testEmptyFieldValueIsRejected() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + Message message = parse(dictionary, VALID_ORDER.replace("55=QFJ\001", "55=QFJ\00158=\001")); + DataDictionaryValidator validator = new DataDictionaryValidator(new ValidationSettings()); + + FieldException e = assertThrows(FieldException.class, + () -> validator.validate(dictionary, message)); + assertEquals(SessionRejectReason.TAG_SPECIFIED_WITHOUT_A_VALUE, e.getSessionRejectReason()); + } + + @Test + public void testEmptyFieldValueIsAcceptedWhenCheckDisabled() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + Message message = parse(dictionary, VALID_ORDER.replace("55=QFJ\001", "55=QFJ\00158=\001")); + ValidationSettings settings = new ValidationSettings(); + settings.setCheckFieldsHaveValues(false); + + new DataDictionaryValidator(settings).validate(dictionary, message); + } + + @Test + public void testGroupCountMismatchIsRejected() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + // NoAllocs(78) declares two groups but only one is present + Message message = parse(dictionary, VALID_ORDER); + Group alloc = new Group(78, 79); + alloc.setString(79, "allocAccount"); + message.addGroup(alloc); + message.setString(78, "2"); + DataDictionaryValidator validator = new DataDictionaryValidator(new ValidationSettings()); + + FieldException e = assertThrows(FieldException.class, + () -> validator.validate(dictionary, message)); + assertEquals(SessionRejectReason.INCORRECT_NUMINGROUP_COUNT_FOR_REPEATING_GROUP, + e.getSessionRejectReason()); + } + + @Test + public void testStoredParseExceptionIsRethrown() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + // header field SenderCompID(49) in the body is stored as a deferred exception during parse + Message message = new Message(); + message.fromString(VALID_ORDER.replace("49=SENDER\001", "").replace("55=QFJ\001", + "55=QFJ\00149=SENDER\001"), dictionary, new ValidationSettings(), true, false); + DataDictionaryValidator validator = new DataDictionaryValidator(new ValidationSettings()); + + assertFalse(message.hasValidStructure()); + assertThrows(FieldException.class, () -> validator.validate(dictionary, message)); + } + + @Test + public void testFixt11MessageIsNotRejectedForVersionMismatch() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + Message message = parse(dictionary, VALID_ORDER.replace("8=FIX.4.4\001", "8=FIXT.1.1\001")); + + new DataDictionaryValidator(new ValidationSettings()).validate(message, dictionary, dictionary); + } + + @Test + public void testMultipleCharValueFormat() throws Exception { + DataDictionary dictionary = buildDictionary("4", "4", + ""); + + new DataDictionaryValidator(new ValidationSettings()) + .validate(dictionary, buildMessage("FIX.4.4", "A B")); + } + + @Test + public void testUtcDateFormat() throws Exception { + DataDictionary dictionary = buildDictionary("4", "4", + ""); + + new DataDictionaryValidator(new ValidationSettings()) + .validate(dictionary, buildMessage("FIX.4.4", "20260819")); + } + + @Test + public void testCharFieldIsNotConvertedInOldFixVersions() throws Exception { + // before FIX 4.2 a CHAR field is treated as a String, so a multi-char value is valid + DataDictionary dictionary = buildDictionary("4", "0", + ""); + + new DataDictionaryValidator(new ValidationSettings()) + .validate(dictionary, buildMessage("FIX.4.0", "ZZ")); + } + + @Test + public void testFix50DictionaryAcceptsOtherMessageVersions() throws Exception { + DataDictionary dictionary = buildDictionary("5", "0", + ""); + Message message = buildMessage("FIX.4.4", "x"); + + new DataDictionaryValidator(new ValidationSettings()).validate(message, dictionary, dictionary); + } + + private DataDictionary buildDictionary(String major, String minor, String fieldDefinition) + throws ConfigError { + String data = ""; + data += ""; + data += "
"; + data += " "; + data += " "; + data += "
"; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += fieldDefinition; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
"; + return new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + private Message buildMessage(String beginString, String testFieldValue) { + Message message = new Message(); + message.getHeader().setString(8, beginString); + message.getHeader().setString(35, "U1"); + message.setString(18, testFieldValue); + message.getTrailer().setString(10, "000"); + return message; + } + + private Message parse(DataDictionary dictionary, String data) throws Exception { + Message message = new Message(); + message.fromString(data, dictionary, new ValidationSettings(), false); + return message; + } +} From d770386c3bd1eda1133c812cccd07dbf92588fd5 Mon Sep 17 00:00:00 2001 From: konradbloor Date: Sat, 15 Aug 2026 10:32:21 +0100 Subject: [PATCH 2/3] Replace package-private static `DataDictionary.validate()` with `DataDictionaryValidator` at call sites --- .../main/java/quickfix/DataDictionary.java | 6 ----- .../src/main/java/quickfix/Session.java | 6 +++-- .../src/test/java/quickfix/MessageTest.java | 22 +++++++++---------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/quickfixj-base/src/main/java/quickfix/DataDictionary.java b/quickfixj-base/src/main/java/quickfix/DataDictionary.java index cafe3e487..0390cf48b 100644 --- a/quickfixj-base/src/main/java/quickfix/DataDictionary.java +++ b/quickfixj-base/src/main/java/quickfix/DataDictionary.java @@ -613,12 +613,6 @@ public void validate(Message message, boolean bodyOnly, ValidationSettings setti new DataDictionaryValidator(settings).validate(this, message, bodyOnly); } - static void validate(Message message, DataDictionary sessionDataDictionary, - DataDictionary applicationDataDictionary, ValidationSettings settings) throws IncorrectTagValue, FieldNotFound, - IncorrectDataFormat { - new DataDictionaryValidator(settings).validate(message, sessionDataDictionary, applicationDataDictionary); - } - /** Check if this dictionary was loaded with a FIX version. **/ boolean hasVersion() { return hasVersion; diff --git a/quickfixj-core/src/main/java/quickfix/Session.java b/quickfixj-core/src/main/java/quickfix/Session.java index 87e6451f9..446b704c9 100644 --- a/quickfixj-core/src/main/java/quickfix/Session.java +++ b/quickfixj-core/src/main/java/quickfix/Session.java @@ -428,6 +428,7 @@ public class Session implements Closeable { private final DataDictionaryProvider dataDictionaryProvider; private final ValidationSettings validationSettings; + private final DataDictionaryValidator dataDictionaryValidator; private final boolean checkLatency; private final int maxLatency; private int resendRequestChunkSize = 0; @@ -552,6 +553,7 @@ public class Session implements Closeable { this.refreshOnLogon = refreshOnLogon; this.dataDictionaryProvider = dataDictionaryProvider; this.validationSettings = validationSettings; + this.dataDictionaryValidator = new DataDictionaryValidator(validationSettings); this.messageFactory = messageFactory; this.checkCompID = checkCompID; this.redundantResentRequestsAllowed = redundantResentRequestsAllowed; @@ -1074,8 +1076,8 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi // related to QFJ-367 : just warn invalid incoming field/tags try { - DataDictionary.validate(message, sessionDataDictionary, - applicationDataDictionary, validationSettings); + dataDictionaryValidator.validate(message, + sessionDataDictionary, applicationDataDictionary); } catch (final IncorrectTagValue e) { if (rejectInvalidMessage) { throw e; diff --git a/quickfixj-core/src/test/java/quickfix/MessageTest.java b/quickfixj-core/src/test/java/quickfix/MessageTest.java index 86326d80a..978c9397f 100644 --- a/quickfixj-core/src/test/java/quickfix/MessageTest.java +++ b/quickfixj-core/src/test/java/quickfix/MessageTest.java @@ -342,7 +342,7 @@ public void testAppMessageValidation() throws Exception { assertNotNull(sessDictionary); assertNotNull(appDictionary); mdsfr.fromString(data, sessDictionary, appDictionary, new ValidationSettings(), true); - DataDictionary.validate(mdsfr, sessDictionary, appDictionary, new ValidationSettings()); + new DataDictionaryValidator(new ValidationSettings()).validate(mdsfr, sessDictionary, appDictionary); } @Test @@ -357,7 +357,7 @@ public void testAppMessageValidationFixLatest() throws Exception { assertNotNull(appDictionary); ValidationSettings dds = new ValidationSettings(); mdsfr.fromString(data, sessDictionary, appDictionary, dds, true); - DataDictionary.validate(mdsfr, sessDictionary, appDictionary, dds); + new DataDictionaryValidator(dds).validate(mdsfr, sessDictionary, appDictionary); } @Test @@ -370,7 +370,7 @@ public void testAdminMessageValidation() throws Exception { assertNotNull(sessionDictionary); assertNotNull(appDictionary); logon.fromString(data, sessionDictionary, appDictionary, new ValidationSettings(), true); - DataDictionary.validate(logon, sessionDictionary, sessionDictionary, new ValidationSettings()); + new DataDictionaryValidator(new ValidationSettings()).validate(logon, sessionDictionary, sessionDictionary); } @Test @@ -1316,7 +1316,7 @@ public void testValidateFieldsOutOfOrderFIXT11() throws Exception { "10=129\u0001"; final TradeCaptureReport tcrOrdered = new TradeCaptureReport(); tcrOrdered.fromString(orderedData, sessDictionary, appDictionary, dds, true); - DataDictionary.validate(tcrOrdered, sessDictionary, appDictionary, dds); + new DataDictionaryValidator(dds).validate(tcrOrdered, sessDictionary, appDictionary); // As this is our reference message created with all validations switched on, make sure some message components // are as expected assertEquals(tcrOrdered.getHeader().getGroupCount(NoHops.FIELD), 2); @@ -1335,7 +1335,7 @@ public void testValidateFieldsOutOfOrderFIXT11() throws Exception { "10=129\u0001"; TradeCaptureReport tcrUnOrdered = new TradeCaptureReport(); tcrUnOrdered.fromString(unorderedData, sessDictionary, appDictionary, dds, true); - DataDictionary.validate(tcrUnOrdered, sessDictionary, appDictionary, dds); + new DataDictionaryValidator(dds).validate(tcrUnOrdered, sessDictionary, appDictionary); assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); @@ -1350,7 +1350,7 @@ public void testValidateFieldsOutOfOrderFIXT11() throws Exception { "10=129\u0001"; tcrUnOrdered = new TradeCaptureReport(); tcrUnOrdered.fromString(unorderedData, sessDictionary, appDictionary, dds, true); - DataDictionary.validate(tcrUnOrdered, sessDictionary, appDictionary, dds); + new DataDictionaryValidator(dds).validate(tcrUnOrdered, sessDictionary, appDictionary); assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); @@ -1367,7 +1367,7 @@ public void testValidateFieldsOutOfOrderFIXT11() throws Exception { "10=129\u0001"; tcrUnOrdered = new TradeCaptureReport(); tcrUnOrdered.fromString(unorderedData, sessDictionary, appDictionary, dds, true); - DataDictionary.validate(tcrUnOrdered, sessDictionary, appDictionary, dds); + new DataDictionaryValidator(dds).validate(tcrUnOrdered, sessDictionary, appDictionary); assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); @@ -1390,7 +1390,7 @@ public void testValidateFieldsOutOfOrderPreFIXT11() throws Exception { + "10=191\u0001"; final TradeCaptureReport tcrOrdered = new TradeCaptureReport(); tcrOrdered.fromString(orderedData, sessDictionary, dds, true); - DataDictionary.validate(tcrOrdered, sessDictionary, sessDictionary, dds); + new DataDictionaryValidator(dds).validate(tcrOrdered, sessDictionary, sessDictionary); // As this is our reference message created with all validations switched on, // make sure some message components @@ -1411,7 +1411,7 @@ public void testValidateFieldsOutOfOrderPreFIXT11() throws Exception { + "10=191\u0001"; TradeCaptureReport tcrUnOrdered = new TradeCaptureReport(); tcrUnOrdered.fromString(unorderedData, sessDictionary, dds, true); - DataDictionary.validate(tcrUnOrdered, sessDictionary, sessDictionary, dds); + new DataDictionaryValidator(dds).validate(tcrUnOrdered, sessDictionary, sessDictionary); assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); @@ -1427,7 +1427,7 @@ public void testValidateFieldsOutOfOrderPreFIXT11() throws Exception { + "10=191\u0001"; tcrUnOrdered = new TradeCaptureReport(); tcrUnOrdered.fromString(unorderedData, sessDictionary, dds, true); - DataDictionary.validate(tcrUnOrdered, sessDictionary, sessDictionary, dds); + new DataDictionaryValidator(dds).validate(tcrUnOrdered, sessDictionary, sessDictionary); assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); @@ -1445,7 +1445,7 @@ public void testValidateFieldsOutOfOrderPreFIXT11() throws Exception { + "10=191\u0001"; tcrUnOrdered = new TradeCaptureReport(); tcrUnOrdered.fromString(unorderedData, sessDictionary, dds, true); - DataDictionary.validate(tcrUnOrdered, sessDictionary, sessDictionary, dds); + new DataDictionaryValidator(dds).validate(tcrUnOrdered, sessDictionary, sessDictionary); assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); } From 69b128bf2e1fc11198e6000ea343803ed7c38e5d Mon Sep 17 00:00:00 2001 From: konradbloor Date: Wed, 19 Aug 2026 17:09:06 +0100 Subject: [PATCH 3/3] Inline group count `FieldException` throws and add repeating group validation tests JaCoCo attributes a call site as missed when the called method throws, so the `throwNewFieldException` helper made `checkGroupCount` appear uncovered even though tests exercised it. Inline the throws and add tests for the matching, mismatched, non-integer, and undefined-group cases. --- .../quickfix/DataDictionaryValidator.java | 14 +++--- .../quickfix/DataDictionaryValidatorTest.java | 47 +++++++++++++++++++ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/quickfixj-base/src/main/java/quickfix/DataDictionaryValidator.java b/quickfixj-base/src/main/java/quickfix/DataDictionaryValidator.java index 048aec076..206504ecc 100644 --- a/quickfixj-base/src/main/java/quickfix/DataDictionaryValidator.java +++ b/quickfixj-base/src/main/java/quickfix/DataDictionaryValidator.java @@ -266,20 +266,18 @@ private void checkGroupCount(DataDictionary dd, StringField field, FieldMap fiel if (dd.isGroup(msgType, fieldNum)) { try { if (fieldMap.getGroupCount(fieldNum) != IntConverter.convert(field.getValue())) { - throwNewFieldException(fieldNum); + throw new FieldException( + SessionRejectReason.INCORRECT_NUMINGROUP_COUNT_FOR_REPEATING_GROUP, + fieldNum); } } catch (FieldConvertError ex) { - throwNewFieldException(fieldNum); + throw new FieldException( + SessionRejectReason.INCORRECT_NUMINGROUP_COUNT_FOR_REPEATING_GROUP, + fieldNum); } } } - private void throwNewFieldException(final int fieldNum) throws FieldException { - throw new FieldException( - SessionRejectReason.INCORRECT_NUMINGROUP_COUNT_FOR_REPEATING_GROUP, - fieldNum); - } - /** Check if a message has all required fields. **/ private void checkHasRequired(DataDictionary dd, FieldMap header, FieldMap body, FieldMap trailer, String msgType, boolean bodyOnly) { diff --git a/quickfixj-base/src/test/java/quickfix/DataDictionaryValidatorTest.java b/quickfixj-base/src/test/java/quickfix/DataDictionaryValidatorTest.java index 00a1360f4..1efe71c23 100644 --- a/quickfixj-base/src/test/java/quickfix/DataDictionaryValidatorTest.java +++ b/quickfixj-base/src/test/java/quickfix/DataDictionaryValidatorTest.java @@ -213,6 +213,53 @@ public void testGroupCountMismatchIsRejected() throws Exception { e.getSessionRejectReason()); } + @Test + public void testMatchingGroupCountIsAccepted() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + Message message = parse(dictionary, VALID_ORDER); + Group alloc = new Group(78, 79); + alloc.setString(79, "allocAccount"); + message.addGroup(alloc); + + new DataDictionaryValidator(new ValidationSettings()).validate(dictionary, message); + } + + @Test + public void testNonIntegerGroupCountIsRejected() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + // an empty NoAllocs(78) passes the format check when checkFieldsHaveValues is + // disabled, so the conversion failure surfaces in the group count check + Message message = parse(dictionary, VALID_ORDER); + Group alloc = new Group(78, 79); + alloc.setString(79, "allocAccount"); + message.addGroup(alloc); + message.setString(78, ""); + ValidationSettings settings = new ValidationSettings(); + settings.setCheckFieldsHaveValues(false); + DataDictionaryValidator validator = new DataDictionaryValidator(settings); + + FieldException e = assertThrows(FieldException.class, + () -> validator.validate(dictionary, message)); + assertEquals(SessionRejectReason.INCORRECT_NUMINGROUP_COUNT_FOR_REPEATING_GROUP, + e.getSessionRejectReason()); + } + + @Test + public void testGroupNotDefinedForMessageTypeIsRejected() throws Exception { + DataDictionary dictionary = DataDictionaryTest.getDictionary(); + // NoMDEntries(268) is a valid FIX 4.4 group but not defined for NewOrderSingle + Message message = parse(dictionary, VALID_ORDER); + Group mdEntry = new Group(268, 269); + mdEntry.setString(269, "0"); + message.addGroup(mdEntry); + DataDictionaryValidator validator = new DataDictionaryValidator(new ValidationSettings()); + + FieldException e = assertThrows(FieldException.class, + () -> validator.validate(dictionary, message)); + assertEquals(SessionRejectReason.TAG_NOT_DEFINED_FOR_THIS_MESSAGE_TYPE, + e.getSessionRejectReason()); + } + @Test public void testStoredParseExceptionIsRethrown() throws Exception { DataDictionary dictionary = DataDictionaryTest.getDictionary();