uk.co.real_logic.sbe.xml.Message Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sbe-all Show documentation
Show all versions of sbe-all Show documentation
FIX/SBE - OSI layer 6 presentation for encoding and decoding application messages in binary format for low-latency applications.
/*
* Copyright 2013-2024 Real Logic Limited.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package uk.co.real_logic.sbe.xml;
import org.agrona.collections.IntHashSet;
import org.agrona.collections.ObjectHashSet;
import uk.co.real_logic.sbe.ir.Token;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static javax.xml.xpath.XPathConstants.NODESET;
import static uk.co.real_logic.sbe.xml.XmlSchemaParser.*;
/**
* An SBE message containing a list of {@link Field} objects and SBE message attributes.
*
* What is difference between {@link Message} and the Intermediate Representation (IR)?
*
* - IR is intentionally platform, schema, and language independent.
* - IR is abstract layout and encoding only.
* - IR is a flat representation without cycles or hierarchy.
* - Message is FIX/SBE XML Schema specific.
*
*/
public class Message
{
private static final String FIELD_OR_GROUP_OR_DATA_EXPR = "field|group|data";
private final int id;
private final String name;
private final String description;
private final int sinceVersion;
private final int deprecated;
private final int blockLength;
private final List fieldList;
private final String semanticType;
private final int computedBlockLength;
private final Map typeByNameMap;
/**
* Construct a new message from XML Schema.
*
* @param messageNode from the XML Schema Parsing
* @param typeByNameMap holding type information for message
* @throws XPathExpressionException on invalid XPath
*/
public Message(final Node messageNode, final Map typeByNameMap) throws XPathExpressionException
{
id = Integer.parseInt(getAttributeValue(messageNode, "id")); // required
name = getAttributeValue(messageNode, "name"); // required
description = getAttributeValueOrNull(messageNode, "description"); // optional
blockLength = Integer.parseInt(getAttributeValue(messageNode, "blockLength", "0")); // 0 means not set
sinceVersion = Integer.parseInt(getAttributeValue(messageNode, "sinceVersion", "0"));
deprecated = Integer.parseInt(getAttributeValue(messageNode, "deprecated", "0"));
semanticType = getAttributeValueOrNull(messageNode, "semanticType"); // optional
this.typeByNameMap = typeByNameMap;
fieldList = parseMembers(messageNode);
computeAndValidateOffsets(messageNode, fieldList, blockLength);
computedBlockLength = computeMessageRootBlockLength(fieldList);
validateBlockLength(messageNode, blockLength, computedBlockLength);
}
Message(final Message message, final List newFieldList)
{
id = message.id;
name = message.name;
description = message.description;
blockLength = message.blockLength;
sinceVersion = message.sinceVersion;
deprecated = message.deprecated;
semanticType = message.semanticType;
typeByNameMap = message.typeByNameMap;
fieldList = newFieldList;
computedBlockLength = computeMessageRootBlockLength(newFieldList);
}
/**
* Return the template schemaId of the message
*
* @return schemaId of the message
*/
public int id()
{
return id;
}
/**
* Return the name of the message
*
* @return name of the message
*/
public String name()
{
return name;
}
/**
* The description of the message (if set) or null
*
* @return description set by the message or null
*/
public String description()
{
return description;
}
/**
* The semanticType of the message (if set) or null
*
* @return the semanticType of the message (if set) or null
*/
public String semanticType()
{
return semanticType;
}
/**
* The version since this was added to the template.
*
* @return version since this was added to the template.
*/
public int sinceVersion()
{
return sinceVersion;
}
/**
* Version in which message was deprecated. Only valid if greater than zero.
*
* @return version in which the message was deprecated.
*/
public int deprecated()
{
return deprecated;
}
/**
* Return the list of fields in the message
*
* @return {@link java.util.List} of the Field objects in this Message
*/
public List fields()
{
return fieldList;
}
/**
* Return the encodedLength of the {@link Message} in bytes including any padding.
*
* @return the encodedLength of the {@link Message} in bytes including any padding.
*/
public int blockLength()
{
return Math.max(blockLength, computedBlockLength);
}
private List parseMembers(final Node node) throws XPathExpressionException
{
final XPath xPath = XPathFactory.newInstance().newXPath();
final NodeList list = (NodeList)xPath.compile(FIELD_OR_GROUP_OR_DATA_EXPR).evaluate(node, NODESET);
boolean groupEncountered = false, dataEncountered = false;
final ObjectHashSet distinctNames = new ObjectHashSet<>();
final IntHashSet distinctIds = new IntHashSet();
final ArrayList fieldList = new ArrayList<>();
for (int i = 0, size = list.getLength(); i < size; i++)
{
final Field field;
final String nodeName = list.item(i).getNodeName();
switch (nodeName)
{
case "group":
if (dataEncountered)
{
handleError(node, "group node specified after data node");
}
field = parseGroupField(list, i);
groupEncountered = true;
break;
case "data":
field = parseDataField(list, i);
dataEncountered = true;
break;
case "field":
if (groupEncountered || dataEncountered)
{
handleError(node, "field node specified after group or data node specified");
}
field = parseField(list, i);
break;
default:
throw new IllegalStateException("Unknown node name: " + nodeName);
}
if (!distinctIds.add(field.id()))
{
handleError(node, "duplicate id found: " + field.id());
}
if (!distinctNames.add(field.name()))
{
handleError(node, "duplicate name found: " + field.name());
}
fieldList.add(field);
}
return fieldList;
}
private Field parseGroupField(final NodeList nodeList, final int nodeIndex) throws XPathExpressionException
{
final Node node = nodeList.item(nodeIndex);
final String dimensionTypeName = getAttributeValue(node, "dimensionType", "groupSizeEncoding");
Type dimensionType = typeByNameMap.get(dimensionTypeName);
if (dimensionType == null)
{
handleError(node, "could not find dimensionType: " + dimensionTypeName);
}
else if (!(dimensionType instanceof CompositeType))
{
handleError(node, "dimensionType should be a composite type: " + dimensionTypeName);
dimensionType = null;
}
else
{
((CompositeType)dimensionType).checkForWellFormedGroupSizeEncoding(node);
}
final Field field = new Field.Builder()
.name(getAttributeValue(node, "name"))
.description(getAttributeValueOrNull(node, "description"))
.id(Integer.parseInt(getAttributeValue(node, "id")))
.blockLength(Integer.parseInt(getAttributeValue(node, "blockLength", "0")))
.sinceVersion(Integer.parseInt(getAttributeValue(node, "sinceVersion", "0")))
.deprecated(Integer.parseInt(getAttributeValue(node, "deprecated", "0")))
.dimensionType((CompositeType)dimensionType)
.build();
XmlSchemaParser.checkForValidName(node, field.name());
field.groupFields(parseMembers(node)); // recursive call
return field;
}
private Field parseField(final NodeList nodeList, final int nodeIndex)
{
final Node node = nodeList.item(nodeIndex);
final String typeName = getAttributeValue(node, "type");
final Type fieldType = typeByNameMap.get(typeName);
if (fieldType == null)
{
handleError(node, "could not find type: " + typeName);
}
final Field field = new Field.Builder()
.name(getAttributeValue(node, "name"))
.description(getAttributeValueOrNull(node, "description"))
.id(Integer.parseInt(getAttributeValue(node, "id")))
.offset(Integer.parseInt(getAttributeValue(node, "offset", "0")))
.semanticType(getAttributeValueOrNull(node, "semanticType"))
.presence(getPresence(node, fieldType))
.valueRef(getAttributeValueOrNull(node, "valueRef"))
.sinceVersion(Integer.parseInt(getAttributeValue(node, "sinceVersion", "0")))
.deprecated(Integer.parseInt(getAttributeValue(node, "deprecated", "0")))
.epoch(getAttributeValueOrNull(node, "epoch"))
.timeUnit(getAttributeValueOrNull(node, "timeUnit"))
.type(fieldType)
.build();
field.validate(node, typeByNameMap);
return field;
}
private static Presence getPresence(final Node node, final Type fieldType)
{
final String presenceStr = getAttributeValueOrNull(node, "presence");
final Presence presence;
if (null != presenceStr)
{
presence = Presence.get(presenceStr);
}
else if (null != fieldType)
{
presence = fieldType.presence();
}
else
{
presence = Presence.REQUIRED;
}
return presence;
}
private Field parseDataField(final NodeList nodeList, final int nodeIndex)
{
final Node node = nodeList.item(nodeIndex);
final String typeName = getAttributeValue(node, "type");
final Type fieldType = typeByNameMap.get(typeName);
if (fieldType == null)
{
handleError(node, "could not find type: " + typeName);
}
else if (!(fieldType instanceof CompositeType))
{
handleError(node, "data type is not composite type: " + typeName);
}
else
{
((CompositeType)fieldType).checkForWellFormedVariableLengthDataEncoding(node);
((CompositeType)fieldType).makeDataFieldCompositeType();
}
final Field field = new Field.Builder()
.name(getAttributeValue(node, "name"))
.description(getAttributeValueOrNull(node, "description"))
.id(Integer.parseInt(getAttributeValue(node, "id")))
.offset(Integer.parseInt(getAttributeValue(node, "offset", "0")))
.semanticType(getAttributeValueOrNull(node, "semanticType"))
.presence(Presence.get(getAttributeValue(node, "presence", "required")))
.sinceVersion(Integer.parseInt(getAttributeValue(node, "sinceVersion", "0")))
.deprecated(Integer.parseInt(getAttributeValue(node, "deprecated", "0")))
.epoch(getAttributeValueOrNull(node, "epoch"))
.timeUnit(getAttributeValueOrNull(node, "timeUnit"))
.type(fieldType)
.variableLength(true)
.build();
field.validate(node, typeByNameMap);
return field;
}
/*
* Compute and validate the offsets of the fields in the list and will set the fields computedOffset.
* Will validate the blockLength of the fields encompassing <message> or <group> and recursively
* descend into repeated groups.
*/
private static int computeAndValidateOffsets(final Node node, final List fields, final int blockLength)
{
boolean variableLengthBlock = false;
int offset = 0;
for (final Field field : fields)
{
if (0 != field.offset() && field.offset() < offset)
{
handleError(node, "Offset provides insufficient space at field: " + field.name());
}
if (Token.VARIABLE_LENGTH != offset)
{
if (0 != field.offset())
{
offset = field.offset();
}
else if (null != field.dimensionType() && 0 != blockLength)
{
offset = blockLength;
}
else if (field.isVariableLength() && 0 != blockLength)
{
offset = blockLength;
}
}
field.computedOffset(variableLengthBlock ? Token.VARIABLE_LENGTH : offset);
if (null != field.groupFields())
{
final int groupBlockLength = computeAndValidateOffsets(node, field.groupFields(), 0);
validateBlockLength(node, field.blockLength(), groupBlockLength);
field.computedBlockLength(Math.max(field.blockLength(), groupBlockLength));
variableLengthBlock = true;
}
else if (null != field.type() && Presence.CONSTANT != field.presence())
{
final int size = field.type().encodedLength();
if (Token.VARIABLE_LENGTH == size)
{
variableLengthBlock = true;
}
else
{
field.computedBlockLength(size);
}
if (!variableLengthBlock)
{
offset += size;
}
}
}
return offset;
}
private static int computeMessageRootBlockLength(final List fields)
{
int blockLength = 0;
for (final Field field : fields)
{
if (field.groupFields() != null)
{
return blockLength;
}
else if (field.type() != null)
{
final int fieldLength = field.type().encodedLength();
if (Token.VARIABLE_LENGTH == fieldLength)
{
return blockLength;
}
if (field.presence() == Presence.CONSTANT)
{
blockLength = field.computedOffset();
}
else
{
blockLength = field.computedOffset() + fieldLength;
}
}
}
return blockLength;
}
private static void validateBlockLength(
final Node node, final long specifiedBlockLength, final long computedBlockLength)
{
if (0 != specifiedBlockLength && computedBlockLength > specifiedBlockLength)
{
final String msg = "specified blockLength provides insufficient space " +
computedBlockLength + " > " + specifiedBlockLength;
handleError(node, msg);
}
}
/**
* {@inheritDoc}
*/
public String toString()
{
return "Message{" +
"id=" + id +
", name='" + name + '\'' +
", description='" + description + '\'' +
", sinceVersion=" + sinceVersion +
", deprecated=" + deprecated +
", blockLength=" + blockLength +
", fieldList=" + fieldList +
", semanticType='" + semanticType + '\'' +
", computedBlockLength=" + computedBlockLength +
", typeByNameMap=" + typeByNameMap +
'}';
}
}