/*******************************************************************************
* 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 quickfix.FileUtil.Location.CLASSLOADER_RESOURCE;
import static quickfix.FileUtil.Location.CONTEXT_RESOURCE;
import static quickfix.FileUtil.Location.FILESYSTEM;
import static quickfix.FileUtil.Location.URL;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
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.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;
/**
* Provide the message metadata for various versions of FIX.
*
*/
public class DataDictionary {
private static final String FIXT_PREFIX = "FIXT";
private static final String FIX_PREFIX = "FIX";
public static final String ANY_VALUE = "__ANY__";
public static final String HEADER_ID = "HEADER";
public static final String TRAILER_ID = "TRAILER";
private static final String MESSAGE_CATEGORY_ADMIN = "admin".intern();
private static final String MESSAGE_CATEGORY_APP = "app".intern();
private static final int USER_DEFINED_TAG_MIN = 5000;
private static final String NO = "N";
private boolean hasVersion = false;
private boolean checkFieldsOutOfOrder = true;
private boolean checkFieldsHaveValues = true;
private boolean checkUserDefinedFields = true;
private boolean checkUnorderedGroupFields = true;
private boolean allowUnknownMessageFields = false;
private String beginString;
private final Map<String, Set<Integer>> messageFields = new HashMap<String, Set<Integer>>();
private final Map<String, Set<Integer>> requiredFields = new HashMap<String, Set<Integer>>();
private final Set<String> messages = new HashSet<String>();
private final Map<String, String> messageCategory = new HashMap<String, String>();
private final Map<String, String> messageTypeForName = new HashMap<String, String>();
private final LinkedHashSet<Integer> fields = new LinkedHashSet<Integer>();
private final Map<Integer, FieldType> fieldTypes = new HashMap<Integer, FieldType>();
private final Map<Integer, Set<String>> fieldValues = new HashMap<Integer, Set<String>>();
private final Map<Integer, String> fieldNames = new HashMap<Integer, String>();
private final Map<String, Integer> names = new HashMap<String, Integer>();
private final Map<IntStringPair, String> valueNames = new HashMap<IntStringPair, String>();
private final Map<IntStringPair, GroupInfo> groups = new HashMap<IntStringPair, GroupInfo>();
private final Map<String, Node> components = new HashMap<String, Node>();
private DataDictionary() {
}
/**
* Initialize a data dictionary from a URL or a file path.
*
* @param location
* a URL or file system path
* @throws ConfigError
*/
public DataDictionary(String location) throws ConfigError {
read(location);
}
/**
* Initialize a data dictionary from an input stream.
*
* @param in
* the input stream
* @throws ConfigError
*/
public DataDictionary(InputStream in) throws ConfigError {
load(in);
}
/**
* Copy a data dictionary.
*
* @param source
* the source dictionary that will be copied into this dictionary
*/
public DataDictionary(DataDictionary source) {
copyFrom(source);
}
private void setVersion(String beginString) {
this.beginString = beginString;
hasVersion = true;
}
/**
* Get the FIX version associated with this dictionary.
*
* @return the FIX version
*/
public String getVersion() {
return beginString;
}
private void addField(int field) {
fields.add(field);
}
private void addFieldName(int field, String name) throws ConfigError {
if (names.put(name, field) != null) {
throw new ConfigError("Field named " + name + " defined multiple times");
}
fieldNames.put(field, name);
}
/**
* Get the field name for a specified tag.
*
* @param field
* the tag
* @return the field name
*/
public String getFieldName(int field) {
return fieldNames.get(field);
}
private void addValueName(int field, String value, String name) {
valueNames.put(new IntStringPair(field, value), name);
}
/**
* Get the value name, if any, for an enumerated field value.
*
* @param field
* the tag
* @param value
* the value
* @return the value's name
*/
public String getValueName(int field, String value) {
return valueNames.get(new IntStringPair(field, value));
}
/**
* Predicate for determining if a tag is a defined field.
*
* @param field
* the tag
* @return true if the field is defined, false otherwise
*/
public boolean isField(int field) {
return fields.contains(field);
}
/**
* Return the field type for a field.
*
* @param field
* the tag
* @return the field type
*/
public FieldType getFieldTypeEnum(int field) {
return fieldTypes.get(field);
}
private void addMsgType(String msgType, String msgName) {
messages.add(msgType);
if (msgName != null) {
messageTypeForName.put(msgName, msgType);
}
}
/**
* Return the message type for the specified name.
* @param msgName The message name.
* @return the message type
*/
public String getMsgType(String msgName) {
return messageTypeForName.get(msgName);
}
/**
* Predicate for determining if message type is valid for a specified FIX
* version.
*
* @param msgType
* the message type value
* @return true if the message type if defined, false otherwise
*/
public boolean isMsgType(String msgType) {
return messages.contains(msgType);
}
/**
* Predicate for determining if a message is in the admin category.
* @param msgType the messageType
* @return true, if the msgType is a AdminMessage
* false, if the msgType is a ApplicationMessage
*/
public boolean isAdminMessage(String msgType) {
// Categories are interned
return messageCategory.get(msgType) == MESSAGE_CATEGORY_ADMIN;
}
/**
* Predicate for determining if a message is in the app category.
* @param msgType the messageType
* @return true, if the msgType is a ApplicationMessage
* false, if the msgType is a AdminMessage
*/
public boolean isAppMessage(String msgType) {
// Categories are interned
return messageCategory.get(msgType) == MESSAGE_CATEGORY_APP;
}
private void addMsgField(String msgType, int field) {
Set<Integer> fields = messageFields.get(msgType);
if (fields == null) {
fields = new HashSet<Integer>();
messageFields.put(msgType, fields);
}
fields.add(field);
}
/**
* Predicate for determining if a field is valid for a given message type.
*
* @param msgType
* the message type
* @param field
* the tag
* @return true if field is defined for message, false otherwise.
*/
public boolean isMsgField(String msgType, int field) {
final Set<Integer> fields = messageFields.get(msgType);
return fields != null && fields.contains(field);
}
/**
* Predicate for determining if field is a header field.
*
* @param field
* the tag
* @return true if field is a header field, false otherwise.
*/
public boolean isHeaderField(int field) {
if (messageFields.get(HEADER_ID) == null) {
return false;
}
return messageFields.get(HEADER_ID).contains(field);
}
/**
* Predicate for determining if field is a trailer field.
*
* @param field
* the tag
* @return true if field is a trailer field, false otherwise.
*/
public boolean isTrailerField(int field) {
return messageFields.get(TRAILER_ID).contains(field);
}
private void addFieldType(int field, FieldType fieldType) {
fieldTypes.put(field, fieldType);
}
/**
* Get the field type for a field.
*
* @param field
* a tag
* @return the field type
* @see #getFieldTypeEnum
*/
public int getFieldType(int field) {
return getFieldTypeEnum(field).getOrdinal();
}
/**
* Get the field tag given a field name.
*
* @param name
* the field name
* @return the tag
*/
public int getFieldTag(String name) {
final Integer tag = names.get(name);
return tag != null ? tag.intValue() : -1;
}
private void addRequiredField(String msgType, int field) {
Set<Integer> fields = requiredFields.get(msgType);
if (fields == null) {
fields = new HashSet<Integer>();
requiredFields.put(msgType, fields);
}
fields.add(field);
}
/**
* Predicate for determining if a field is required for a message type
*
* @param msgType
* the message type
* @param field
* the tag
* @return true if field is required, false otherwise
*/
public boolean isRequiredField(String msgType, int field) {
final Set<Integer> fields = requiredFields.get(msgType);
return fields != null && fields.contains(field);
}
/**
* Predicate for determining if a header field is a required field
* @param field the tag
* @return true if field s required, false otherwise
*/
public boolean isRequiredHeaderField(int field) {
return isRequiredField(HEADER_ID, field);
}
/**
* Predicate for determining if a trailer field is a required field
* @param field the tag
* @return true if field s required, false otherwise
*/
public boolean isRequiredTrailerField(int field) {
return isRequiredField(TRAILER_ID, field);
}
private void addFieldValue(int field, String value) {
Set<String> values = fieldValues.get(field);
if (values == null) {
values = new HashSet<String>();
fieldValues.put(field, values);
}
values.add(value);
}
/**
* Predicate for determining if a field has enumerated values.
*
* @param field
* the tag
* @return true if field is enumerated, false otherwise
*/
public boolean hasFieldValue(int field) {
final Set<String> values = fieldValues.get(field);
return values != null && values.size() > 0;
}
/**
* Predicate for determining if a field value is valid
*
* @param field
* the tag
* @param value
* a possible field value
* @return true if field value is valid, false otherwise
*/
public boolean isFieldValue(int field, String value) {
final Set<String> validValues = fieldValues.get(field);
if (validValues == null || validValues.size() == 0) {
return false;
}
if (validValues.contains(ANY_VALUE)) {
return true;
}
if (!isMultipleValueStringField(field)) {
return validValues.contains(value);
}
// MultipleValueString
final String[] values = value.split(" ");
for (int i = 0; i < values.length; i++) {
if (!validValues.contains(values[i])) {
return false;
}
}
return true;
}
private void addGroup(String msg, int field, int delim, DataDictionary dataDictionary) {
groups.put(new IntStringPair(field, msg), new GroupInfo(delim, dataDictionary));
}
/**
* Predicate for determining if a field is a group count field for a message
* type.
*
* @param msg
* the message type
* @param field
* the tag
* @return true if field starts a repeating group, false otherwise
*/
public boolean isGroup(String msg, int field) {
return groups.containsKey(new IntStringPair(field, msg));
}
/**
* Predicate for determining if a field is a header group count field
*
* @param field
* the tag
* @return true if field starts a repeating group, false otherwise
*/
public boolean isHeaderGroup(int field) {
return groups.containsKey(new IntStringPair(field, HEADER_ID));
}
/**
* Get repeating group metadata.
*
* @param msg
* the message type
* @param field
* the tag
* @return an object containing group-related metadata
*/
public GroupInfo getGroup(String msg, int field) {
return groups.get(new IntStringPair(field, msg));
}
/**
* Predicate for determining if a field is a FIX raw data field.
*
* @param field
* the tag
* @return true if field is a raw data field, false otherwise
*/
public boolean isDataField(int field) {
return fieldTypes.get(field) == FieldType.Data;
}
private boolean isMultipleValueStringField(int field) {
return fieldTypes.get(field) == FieldType.MultipleValueString;
}
/**
* Controls whether out of order fields are checked.
*
* @param flag
* true = checked, false = not checked
*/
public void setCheckFieldsOutOfOrder(boolean flag) {
checkFieldsOutOfOrder = flag;
}
public boolean isCheckFieldsOutOfOrder() {
return checkFieldsOutOfOrder;
}
public boolean isCheckUnorderedGroupFields() {
return checkUnorderedGroupFields;
}
/**
* Controls whether group fields are in the same order
* @param flag true = checked, false = not checked
*/
public void setCheckUnorderedGroupFields(boolean flag) {
checkUnorderedGroupFields = flag;
for (GroupInfo gi : groups.values()) {
gi.getDataDictionary().setCheckUnorderedGroupFields(flag);
}
}
/**
* Controls whether empty field values are checked.
*
* @param flag
* true = checked, false = not checked
*/
public void setCheckFieldsHaveValues(boolean flag) {
checkFieldsHaveValues = flag;
for (GroupInfo gi : groups.values()) {
gi.getDataDictionary().setCheckFieldsHaveValues(flag);
}
}
/**
* Controls whether user defined fields are checked.
*
* @param flag
* true = checked, false = not checked
*/
public void setCheckUserDefinedFields(boolean flag) {
checkUserDefinedFields = flag;
for (GroupInfo gi : groups.values()) {
gi.getDataDictionary().setCheckUserDefinedFields(flag);
}
}
public void setAllowUnknownMessageFields(boolean allowUnknownFields) {
allowUnknownMessageFields = allowUnknownFields;
for (GroupInfo gi : groups.values()) {
gi.getDataDictionary().setAllowUnknownMessageFields(allowUnknownFields);
}
}
private void copyFrom(DataDictionary rhs) {
hasVersion = rhs.hasVersion;
beginString = rhs.beginString;
checkFieldsOutOfOrder = rhs.checkFieldsOutOfOrder;
checkFieldsHaveValues = rhs.checkFieldsHaveValues;
checkUserDefinedFields = rhs.checkUserDefinedFields;
copyMap(messageFields, rhs.messageFields);
copyMap(requiredFields, rhs.requiredFields);
copyCollection(messages, rhs.messages);
copyCollection(fields, rhs.fields);
copyMap(fieldTypes, rhs.fieldTypes);
copyMap(fieldValues, rhs.fieldValues);
copyMap(fieldNames, rhs.fieldNames);
copyMap(names, rhs.names);
copyMap(valueNames, rhs.valueNames);
copyMap(groups, rhs.groups);
copyMap(components, rhs.components);
}
@SuppressWarnings("unchecked")
private <K,V> void copyMap(Map<K,V> lhs, Map<K,V> rhs) {
lhs.clear();
final Iterator<?> entries = rhs.entrySet().iterator();
while (entries.hasNext()) {
final Map.Entry<K,V> entry = (Map.Entry<K,V>) entries.next();
Object value = entry.getValue();
if (value instanceof Collection) {
Collection<V> copy;
try {
copy = (Collection<V>) value.getClass().newInstance();
} catch (final RuntimeException e) {
throw e;
} catch (final java.lang.Exception e) {
throw new RuntimeException(e);
}
copyCollection(copy, (Collection<V>) value);
value = copy;
}
lhs.put(entry.getKey(), (V) value);
}
}
private <V> void copyCollection(Collection<V> lhs, Collection<V> rhs) {
lhs.clear();
lhs.addAll(rhs);
}
/**
* Validate a mesasge, including the header and trailer fields.
*
* @param message
* the message
* @throws IncorrectTagValue
* if a field value is not valid
* @throws FieldNotFound
* if a field cannot be found
* @throws IncorrectDataFormat
*/
public void validate(Message message) throws IncorrectTagValue, FieldNotFound,
IncorrectDataFormat {
validate(message, false);
}
/**
* Validate the message body, with header and trailer fields being validated conditionally.
*
* @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
*/
public void validate(Message message, boolean bodyOnly) throws IncorrectTagValue,
FieldNotFound, IncorrectDataFormat {
validate(message, bodyOnly ? null : this, this);
}
static 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)) {
applicationDataDictionary.checkMsgType(msgType);
applicationDataDictionary.checkHasRequired(message.getHeader(), message,
message.getTrailer(), msgType, bodyOnly);
}
if (!bodyOnly) {
sessionDataDictionary.iterate(message.getHeader(), HEADER_ID, sessionDataDictionary);
sessionDataDictionary.iterate(message.getTrailer(), TRAILER_ID, sessionDataDictionary);
}
applicationDataDictionary.iterate(message, msgType, applicationDataDictionary);
}
private static boolean isVersionSpecified(DataDictionary dd) {
return dd != null && dd.hasVersion;
}
private void iterate(FieldMap map, String msgType, DataDictionary dd) throws IncorrectTagValue,
IncorrectDataFormat {
final Iterator<Field<?>> iterator = map.iterator();
while (iterator.hasNext()) {
final StringField field = (StringField) iterator.next();
checkHasValue(field);
if (hasVersion) {
checkValidFormat(field);
checkValue(field);
}
if (beginString != null && shouldCheckTag(field)) {
dd.checkValidTagNumber(field);
if (map instanceof Message) {
checkIsInMessage(field, msgType);
}
dd.checkGroupCount(field, map, msgType);
}
}
for (final List<Group> groups : map.getGroups().values()) {
for (final Group group : groups) {
iterate(group, msgType, dd.getGroup(msgType, group.getFieldTag())
.getDataDictionary());
}
}
}
// / Check if message type is defined in spec.
private void checkMsgType(String msgType) {
if (!isMsgType(msgType)) {
// It would be better to include the msgType in exception message
// Doing that will break acceptance tests
throw new FieldException(SessionRejectReason.INVALID_MSGTYPE);
}
}
// / If we need to check for the tag in the dictionary
private boolean shouldCheckTag(Field<?> field) {
if (!checkUserDefinedFields && field.getField() >= USER_DEFINED_TAG_MIN) {
return false;
} else {
return true;
}
}
// / Check if field tag number is defined in spec.
void checkValidTagNumber(Field<?> field) {
if (!fields.contains(Integer.valueOf(field.getTag()))) {
throw new FieldException(SessionRejectReason.INVALID_TAG_NUMBER, field.getField());
}
}
private void checkValidFormat(StringField field) throws IncorrectDataFormat {
try {
final FieldType fieldType = getFieldTypeEnum(field.getTag());
if (fieldType == FieldType.String) {
// String
} else if (fieldType == FieldType.Char) {
if (beginString.compareTo(FixVersions.BEGINSTRING_FIX41) > 0) {
CharConverter.convert(field.getValue());
} else {
// String, for older FIX versions
}
} else if (fieldType == FieldType.Price) {
DoubleConverter.convert(field.getValue());
} else if (fieldType == FieldType.Int) {
IntConverter.convert(field.getValue());
} else if (fieldType == FieldType.Amt) {
DoubleConverter.convert(field.getValue());
} else if (fieldType == FieldType.Qty) {
DoubleConverter.convert(field.getValue());
} else if (fieldType == FieldType.Qty) {
// String
} else if (fieldType == FieldType.MultipleValueString) {
// String
} else if (fieldType == FieldType.Exchange) {
// String
} else if (fieldType == FieldType.Boolean) {
BooleanConverter.convert(field.getValue());
} else if (fieldType == FieldType.LocalMktDate) {
// String
} else if (fieldType == FieldType.Data) {
// String
} else if (fieldType == FieldType.Float) {
DoubleConverter.convert(field.getValue());
} else if (fieldType == FieldType.PriceOffset) {
DoubleConverter.convert(field.getValue());
} else if (fieldType == FieldType.MonthYear) {
// String
} else if (fieldType == FieldType.DayOfMonth) {
// String
} else if (fieldType == FieldType.UtcDate) {
UtcDateOnlyConverter.convert(field.getValue());
} else if (fieldType == FieldType.UtcTimeOnly) {
UtcTimeOnlyConverter.convert(field.getValue());
} else if (fieldType == FieldType.UtcTimeStamp || fieldType == FieldType.Time) {
UtcTimestampConverter.convert(field.getValue());
} else if (fieldType == FieldType.NumInGroup) {
IntConverter.convert(field.getValue());
} else if (fieldType == FieldType.Percentage) {
DoubleConverter.convert(field.getValue());
} else if (fieldType == FieldType.SeqNum) {
IntConverter.convert(field.getValue());
} else if (fieldType == FieldType.Length) {
IntConverter.convert(field.getValue());
} else if (fieldType == FieldType.Country) {
// String
}
} catch (final FieldConvertError e) {
throw new IncorrectDataFormat(field.getTag(), field.getValue());
}
}
private void checkValue(StringField field) throws IncorrectTagValue {
final int tag = field.getField();
if (!hasFieldValue(tag)) {
return;
}
final String value = field.getValue();
if (!isFieldValue(tag, value)) {
throw new IncorrectTagValue(tag);
}
}
// / Check if a field has a value.
private void checkHasValue(StringField field) {
if (checkFieldsHaveValues && field.getValue().length() == 0) {
throw new FieldException(SessionRejectReason.TAG_SPECIFIED_WITHOUT_A_VALUE,
field.getField());
}
}
// / Check if a field is in this message type.
private void checkIsInMessage(Field<?> field, String msgType) {
if (!isMsgField(msgType, field.getField()) && !allowUnknownMessageFields) {
throw new FieldException(SessionRejectReason.TAG_NOT_DEFINED_FOR_THIS_MESSAGE_TYPE,
field.getField());
}
}
// / Check if group count matches number of groups in
private void checkGroupCount(StringField field, FieldMap fieldMap, String msgType) {
final int fieldNum = field.getField();
if (isGroup(msgType, fieldNum)) {
if (fieldMap.getGroupCount(fieldNum) != Integer.parseInt(field.getValue())) {
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<Integer> requiredFieldsForMessage = requiredFields.get(msgType);
if (requiredFieldsForMessage == null || requiredFieldsForMessage.size() == 0) {
return;
}
final Iterator<Integer> fieldItr = requiredFieldsForMessage.iterator();
while (fieldItr.hasNext()) {
final int field = (fieldItr.next()).intValue();
if (!fields.isSetField(field)) {
throw new FieldException(SessionRejectReason.REQUIRED_TAG_MISSING, field);
}
}
final Map<Integer, List<Group>> groups = fields.getGroups();
if (groups.size() > 0) {
final Iterator<Map.Entry<Integer, List<Group>>> groupIter = groups.entrySet()
.iterator();
while (groupIter.hasNext()) {
final Map.Entry<Integer, List<Group>> entry = groupIter.next();
final GroupInfo p = getGroup(msgType, (entry.getKey()).intValue());
if (p != null) {
final List<Group> groupInstances = entry.getValue();
for (int i = 0; i < groupInstances.size(); i++) {
final FieldMap groupFields = groupInstances.get(i);
p.getDataDictionary().checkHasRequired(groupFields, groupFields,
groupFields, msgType, bodyOnly);
}
}
}
}
}
private void read(String location) throws ConfigError {
final InputStream inputStream = FileUtil.open(getClass(), location, URL, FILESYSTEM,
CONTEXT_RESOURCE, CLASSLOADER_RESOURCE);
if (inputStream == null) {
throw new DataDictionary.Exception("Could not find data dictionary: " + location);
}
try {
load(inputStream);
} catch (final java.lang.Exception e) {
throw new ConfigError(location + ": " + e.getMessage(), e);
} finally {
try {
inputStream.close();
} catch (final IOException e) {
throw new ConfigError(e);
}
}
}
private void load(InputStream inputStream) throws ConfigError {
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
Document document;
try {
final DocumentBuilder builder = factory.newDocumentBuilder();
document = builder.parse(inputStream);
} catch (final Throwable e) {
throw new ConfigError("Could not parse data dictionary file", e);
}
final Element documentElement = document.getDocumentElement();
if (!documentElement.getNodeName().equals("fix")) {
throw new ConfigError(
"Could not parse data dictionary file, or no <fix> node found at root");
}
if (!documentElement.hasAttribute("major")) {
throw new ConfigError("major attribute not found on <fix>");
}
if (!documentElement.hasAttribute("minor")) {
throw new ConfigError("minor attribute not found on <fix>");
}
final String dictionaryType = documentElement.hasAttribute("type") ? documentElement
.getAttribute("type") : FIX_PREFIX;
setVersion(dictionaryType + "." + documentElement.getAttribute("major") + "."
+ documentElement.getAttribute("minor"));
// Index Components
final NodeList componentsNode = documentElement.getElementsByTagName("components");
if (componentsNode.getLength() > 0) {
final NodeList componentNodes = componentsNode.item(0).getChildNodes();
for (int i = 0; i < componentNodes.getLength(); i++) {
final Node componentNode = componentNodes.item(i);
if (componentNode.getNodeName().equals("component")) {
final String name = getAttribute(componentNode, "name");
if (name == null) {
throw new ConfigError("<component> does not have a name attribute");
}
components.put(name, componentNode);
}
}
}
// FIELDS
final NodeList fieldsNode = documentElement.getElementsByTagName("fields");
if (fieldsNode.getLength() == 0) {
throw new ConfigError("<fields> section not found in data dictionary");
}
final NodeList fieldNodes = fieldsNode.item(0).getChildNodes();
if (fieldNodes.getLength() == 0) {
throw new ConfigError("No fields defined");
}
for (int i = 0; i < fieldNodes.getLength(); i++) {
final Node fieldNode = fieldNodes.item(i);
if (fieldNode.getNodeName().equals("field")) {
final String name = getAttribute(fieldNode, "name");
if (name == null) {
throw new ConfigError("<field> does not have a name attribute");
}
final String number = getAttribute(fieldNode, "number");
if (number == null) {
throw new ConfigError("<field> " + name + " does not have a number attribute");
}
final int num = Integer.parseInt(number);
final String type = getAttribute(fieldNode, "type");
if (type == null) {
throw new ConfigError("<field> " + name + " does not have a type attribute");
}
addField(num);
addFieldType(num, FieldType.fromName(getVersion(), type));
addFieldName(num, name);
final NodeList valueNodes = fieldNode.getChildNodes();
for (int j = 0; j < valueNodes.getLength(); j++) {
final Node valueNode = valueNodes.item(j);
if (valueNode.getNodeName().equals("value")) {
final String enumeration = getAttribute(valueNode, "enum");
if (enumeration == null) {
throw new ConfigError("<value> does not have enum attribute in field "
+ name);
}
addFieldValue(num, enumeration);
final String description = getAttribute(valueNode, "description");
if (description != null) {
addValueName(num, enumeration, description);
}
}
}
if (fieldValues.containsKey(num)) {
final String allowOtherValues = getAttribute(fieldNode, "allowOtherValues");
if (allowOtherValues != null && Boolean.parseBoolean(allowOtherValues)) {
addFieldValue(num, ANY_VALUE);
}
}
}
}
if (beginString.startsWith(FIXT_PREFIX) || beginString.compareTo(FixVersions.FIX50) < 0) {
// HEADER
final NodeList headerNode = documentElement.getElementsByTagName("header");
if (headerNode.getLength() == 0) {
throw new ConfigError("<header> section not found in data dictionary");
}
load(document, HEADER_ID, headerNode.item(0));
// TRAILER
final NodeList trailerNode = documentElement.getElementsByTagName("trailer");
if (trailerNode.getLength() == 0) {
throw new ConfigError("<trailer> section not found in data dictionary");
}
load(document, TRAILER_ID, trailerNode.item(0));
}
// MSGTYPE
final NodeList messagesNode = documentElement.getElementsByTagName("messages");
if (messagesNode.getLength() == 0) {
throw new ConfigError("<messages> section not found in data dictionary");
}
final NodeList messageNodes = messagesNode.item(0).getChildNodes();
if (messageNodes.getLength() == 0) {
throw new ConfigError("No messages defined");
}
for (int i = 0; i < messageNodes.getLength(); i++) {
final Node messageNode = messageNodes.item(i);
if (messageNode.getNodeName().equals("message")) {
final String msgtype = getAttribute(messageNode, "msgtype");
if (msgtype == null) {
throw new ConfigError("<message> does not have a msgtype attribute");
}
final String msgcat = getAttribute(messageNode, "msgcat");
if (msgcat != null) {
messageCategory.put(msgtype, msgcat.intern());
}
final String name = getAttribute(messageNode, "name");
addMsgType(msgtype, name);
if (name != null) {
addValueName(MsgType.FIELD, msgtype, name);
}
load(document, msgtype, messageNode);
}
}
}
private void load(Document document, String msgtype, Node node) throws ConfigError {
String name;
final NodeList fieldNodes = node.getChildNodes();
if (fieldNodes.getLength() == 0) {
throw new ConfigError("No fields found: msgType=" + msgtype);
}
for (int j = 0; j < fieldNodes.getLength(); j++) {
final Node fieldNode = fieldNodes.item(j);
if (fieldNode.getNodeName().equals("field") || fieldNode.getNodeName().equals("group")) {
name = getAttribute(fieldNode, "name");
if (name == null) {
throw new ConfigError("<field> does not have a name attribute");
}
final int num = lookupXMLFieldNumber(document, name);
addMsgField(msgtype, num);
final String required = getAttribute(fieldNode, "required", NO);
if (required == null) {
throw new ConfigError("<" + fieldNode.getNodeName()
+ "> does not have a 'required' attribute");
}
if (required.equalsIgnoreCase("Y")) {
addRequiredField(msgtype, num);
}
} else if (fieldNode.getNodeName().equals("component")) {
final String required = getAttribute(fieldNode, "required");
if (required == null) {
throw new ConfigError("<component> does not have a 'required' attribute");
}
addXMLComponentFields(document, fieldNode, msgtype, this,
required.equalsIgnoreCase("Y"));
}
if (fieldNode.getNodeName().equals("group")) {
final String required = getAttribute(fieldNode, "required");
if (required == null) {
throw new ConfigError("<group> does not have a 'required' attribute");
}
addXMLGroup(document, fieldNode, msgtype, this, required.equalsIgnoreCase("Y"));
}
}
}
private int[] orderedFieldsArray;
public int[] getOrderedFields() {
if (orderedFieldsArray == null) {
orderedFieldsArray = new int[fields.size()];
final Iterator<Integer> fieldItr = fields.iterator();
int i = 0;
while (fieldItr.hasNext()) {
orderedFieldsArray[i++] = fieldItr.next();
}
}
return orderedFieldsArray;
}
private int lookupXMLFieldNumber(Document document, Node node) throws ConfigError {
final Element element = (Element) node;
if (!element.hasAttribute("name")) {
throw new ConfigError("No name given to field");
}
return lookupXMLFieldNumber(document, element.getAttribute("name"));
}
private int lookupXMLFieldNumber(Document document, String name) throws ConfigError {
final Integer fieldNumber = names.get(name);
if (fieldNumber == null) {
throw new ConfigError("Field " + name + " not defined in fields section");
}
return fieldNumber.intValue();
}
private int addXMLComponentFields(Document document, Node node, String msgtype,
DataDictionary dd, boolean componentRequired) throws ConfigError {
int firstField = 0;
String name = getAttribute(node, "name");
if (name == null) {
throw new ConfigError("No name given to component");
}
final Node componentNode = components.get(name);
if (componentNode == null) {
throw new ConfigError("Component " + name + " not found");
}
final NodeList componentFieldNodes = componentNode.getChildNodes();
for (int i = 0; i < componentFieldNodes.getLength(); i++) {
final Node componentFieldNode = componentFieldNodes.item(i);
if (componentFieldNode.getNodeName().equals("field")
|| componentFieldNode.getNodeName().equals("group")) {
name = getAttribute(componentFieldNode, "name");
if (name == null) {
throw new ConfigError("No name given to field");
}
final int field = lookupXMLFieldNumber(document, name);
if (firstField == 0) {
firstField = field;
}
final String required = getAttribute(componentFieldNode, "required");
if (required.equalsIgnoreCase("Y") && componentRequired) {
addRequiredField(msgtype, field);
}
dd.addField(field);
dd.addMsgField(msgtype, field);
}
if (componentFieldNode.getNodeName().equals("group")) {
final String required = getAttribute(componentFieldNode, "required");
final boolean isRequired = required.equalsIgnoreCase("Y");
addXMLGroup(document, componentFieldNode, msgtype, dd, isRequired);
}
if (componentFieldNode.getNodeName().equals("component")) {
final String required = getAttribute(componentFieldNode, "required");
final boolean isRequired = required.equalsIgnoreCase("Y");
addXMLComponentFields(document, componentFieldNode, msgtype, dd, isRequired);
}
}
return firstField;
}
private void addXMLGroup(Document document, Node node, String msgtype, DataDictionary dd,
boolean groupRequired) throws ConfigError {
final String name = getAttribute(node, "name");
if (name == null) {
throw new ConfigError("No name given to group");
}
final int group = lookupXMLFieldNumber(document, name);
int delim = 0;
int field = 0;
final DataDictionary groupDD = new DataDictionary();
groupDD.setVersion(dd.getVersion());
final NodeList fieldNodeList = node.getChildNodes();
for (int i = 0; i < fieldNodeList.getLength(); i++) {
final Node fieldNode = fieldNodeList.item(i);
if (fieldNode.getNodeName().equals("field")) {
field = lookupXMLFieldNumber(document, fieldNode);
groupDD.addField(field);
final String required = getAttribute(fieldNode, "required");
if (required != null && required.equalsIgnoreCase("Y") && groupRequired) {
groupDD.addRequiredField(msgtype, field);
}
} else if (fieldNode.getNodeName().equals("component")) {
field = addXMLComponentFields(document, fieldNode, msgtype, groupDD, false);
} else if (fieldNode.getNodeName().equals("group")) {
field = lookupXMLFieldNumber(document, fieldNode);
groupDD.addField(field);
final String required = getAttribute(fieldNode, "required");
if (required != null && required.equalsIgnoreCase("Y") && groupRequired) {
groupDD.addRequiredField(msgtype, field);
}
final boolean isRequired = required == null ? false : required
.equalsIgnoreCase("Y");
addXMLGroup(document, fieldNode, msgtype, groupDD, isRequired);
}
if (delim == 0) {
delim = field;
}
}
if (delim != 0) {
dd.addGroup(msgtype, group, delim, groupDD);
}
}
private String getAttribute(Node node, String name) {
return getAttribute(node, name, null);
}
private String getAttribute(Node node, String name, String defaultValue) {
final NamedNodeMap attributes = node.getAttributes();
if (attributes != null) {
final Node namedItem = attributes.getNamedItem(name);
return namedItem != null ? namedItem.getNodeValue() : null;
}
return defaultValue;
}
/**
* Data dictionary-related exception.
*/
public static class Exception extends RuntimeException {
public Exception(Throwable cause) {
super(cause);
}
public Exception(String message) {
super(message);
}
}
private static final class IntStringPair {
private final int intValue;
private final String stringValue;
public IntStringPair(int value, String value2) {
intValue = value;
stringValue = value2;
}
//public int getIntValue() {
// return intValue;
//}
//public String getStringValue() {
// return stringValue;
//}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof IntStringPair)) {
return false;
}
return intValue == ((IntStringPair) other).intValue
&& stringValue.equals(((IntStringPair) other).stringValue);
}
@Override
public int hashCode() {
return stringValue.hashCode() + intValue;
}
/**
* For debugging
*/
@Override
public String toString() {
final StringBuffer b = new StringBuffer();
b.append('(').append(intValue).append(',').append(stringValue).append(')');
return b.toString();
}
}
/**
* Contains meta-data for FIX repeating groups
*/
public static final class GroupInfo {
private final int delimiterField;
private final DataDictionary dataDictionary;
private GroupInfo(int field, DataDictionary dictionary) {
delimiterField = field;
dataDictionary = dictionary;
}
public DataDictionary getDataDictionary() {
return dataDictionary;
}
/**
* Returns the delimiter field used to start a repeating group instance.
*
* @return delimiter field
* @deprecated use getDelimiterField() instead
*/
@Deprecated
public int getDelimeterField() {
return delimiterField;
}
/**
* Returns the delimiter field used to start a repeating group instance.
*
* @return delimiter field
*/
public int getDelimiterField() {
return delimiterField;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof GroupInfo)) {
return false;
}
return delimiterField == ((GroupInfo) other).delimiterField
&& dataDictionary.equals(((GroupInfo) other).dataDictionary);
}
@Override
public int hashCode() {
return delimiterField;
}
}
}