PageRenderTime 47ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/src/lib/FixNet.Core/MessageBuilder.cs

https://gitlab.com/pcwiek/FIXNet
C# | 275 lines | 173 code | 38 blank | 64 comment | 33 complexity | dc01fde320375daf438fbf170a4e2db4 MD5 | raw file
  1. namespace FixNet.Core
  2. {
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Diagnostics;
  6. using System.Linq;
  7. using Fields;
  8. using Exceptions;
  9. using Extensions;
  10. using Internals;
  11. using Parsing;
  12. using Specification;
  13. using Messages;
  14. /// <summary>
  15. /// A message builder, which parses raw FIX text message into <see cref="Message"/>
  16. /// </summary>
  17. public sealed class MessageBuilder
  18. {
  19. private readonly TraceSource log = new TraceSource(nameof(MessageBuilder));
  20. private struct MessageFields
  21. {
  22. /// <summary>
  23. /// Initializes a new instance of the <see cref="MessageFields"/> struct using specified list of fields.
  24. /// </summary>
  25. /// <param name="fields">A list of extracted fields</param>
  26. /// <exception cref="MessageParsingException">Thrown when either <see cref="BeginString"/> or <see cref="MsgType"/> is missing</exception>
  27. public MessageFields(IList<Field> fields)
  28. {
  29. Fields = fields;
  30. var fixVer = fields.FirstOrDefault(f => f.Tag == BeginString.Tag);
  31. if (fixVer == null)
  32. {
  33. throw new MessageParsingException("BeginString is missing.");
  34. }
  35. var msgType = fields.FirstOrDefault(f => f.Tag == MsgType.Tag);
  36. if (msgType == null)
  37. {
  38. throw new MessageParsingException("MsgType is missing.");
  39. }
  40. FixVersion = new BeginString(((Field.String)fixVer).Value);
  41. MessageType = new MsgType(((Field.String)msgType).Value);
  42. }
  43. /// <summary>
  44. /// Gets the list of all fields.
  45. /// </summary>
  46. public IList<Field> Fields { get; }
  47. /// <summary>
  48. /// Gets the FIX version of the message
  49. /// </summary>
  50. public BeginString FixVersion { get; }
  51. /// <summary>
  52. /// Gets the type of the message
  53. /// </summary>
  54. public MsgType MessageType { get; }
  55. }
  56. /// <summary>
  57. /// Message verification options
  58. /// </summary>
  59. [Flags]
  60. public enum VerificationOptions
  61. {
  62. /// <summary>
  63. /// No verification will be done.
  64. /// </summary>
  65. None,
  66. /// <summary>
  67. /// Indicates whether to check the header field order.
  68. /// </summary>
  69. CheckHeaderFieldOrder = 1 << 0,
  70. /// <summary>
  71. /// Indicates whether to check if the required fields are missing in either header, trailer or the message body.
  72. /// </summary>
  73. CheckRequiredFields = 1 << 1,
  74. /// <summary>
  75. /// Performs full verification check.
  76. /// </summary>
  77. Full = CheckHeaderFieldOrder | CheckRequiredFields
  78. }
  79. private readonly SpecificationData specificationData;
  80. private readonly VerificationOptions options;
  81. private readonly Parser parser;
  82. /// <summary>
  83. /// Initializes a new instance of the <see cref="MessageBuilder"/> class.
  84. /// </summary>
  85. /// <param name="specificationData">FIX specification data</param>
  86. /// <param name="options">Verification options. Set to <c>null</c> to skip verification.</param>
  87. /// <param name="customFieldMapping">Maping for custom fields between their types and parsing funcions</param>
  88. public MessageBuilder(
  89. SpecificationData specificationData,
  90. VerificationOptions options,
  91. IDictionary<FieldType, CustomFieldParser> customFieldMapping)
  92. {
  93. this.specificationData = specificationData;
  94. this.options = options;
  95. parser = new Parser(specificationData, customFieldMapping);
  96. }
  97. public MessageBuilder() : this(SpecificationData.Empty, VerificationOptions.None, new Dictionary<FieldType, CustomFieldParser>())
  98. {
  99. }
  100. /// <summary>
  101. /// Builds a FIX message from text.
  102. /// </summary>
  103. /// <param name="value">Text representation of the FIX message</param>
  104. /// <exception cref="MessageParsingException">Thrown when either <see cref="BeginString"/> or <see cref="MsgType"/> are missing in the message.</exception>
  105. /// <exception cref="MessageVerificationException">Thrown when the verification for the message fails.</exception>
  106. /// <exception cref="VerificationPreconditionException">Thrown when the <see cref="VerificationOptions"/> are set, but preconditions are not met. For example, when the specification data is not available.</exception>
  107. /// <returns>An appropriate instance of <see cref="Message"/></returns>
  108. public Message Build(string value)
  109. {
  110. var extractedFields = ExtractFields(value);
  111. var message = new Message();
  112. var messageSpec = GetSpecification(extractedFields.MessageType);
  113. if (options != VerificationOptions.None)
  114. {
  115. if (specificationData.Header == null)
  116. {
  117. throw new VerificationPreconditionException(
  118. "Verification options are set, but header specification couldn't be extracted from XML file.");
  119. }
  120. if (specificationData.Trailer == null)
  121. {
  122. throw new VerificationPreconditionException(
  123. "Verification options are set, but trailer specification couldn't be extracted from XML file.");
  124. }
  125. }
  126. foreach (var field in extractedFields.Fields)
  127. {
  128. if ((specificationData.Header == null && message.Header.OrderedFieldTags.Contains(field.Tag))
  129. || (specificationData.Header?.ContainsField(field.Tag) ?? false))
  130. {
  131. message.Header.Set(field);
  132. }
  133. else if ((specificationData.Trailer == null && message.Trailer.OrderedFieldTags.Contains(field.Tag)) ||
  134. (specificationData.Trailer?.ContainsField(field.Tag) ?? false))
  135. {
  136. message.Trailer.Set(field);
  137. }
  138. else
  139. {
  140. message.Set(field);
  141. }
  142. }
  143. Verify(extractedFields.Fields, message, messageSpec);
  144. return message;
  145. }
  146. /// <summary>
  147. /// Extracts all fields from the message, including the <see cref="BeginString"/> and <see cref="MsgType"/>.
  148. /// </summary>
  149. /// <param name="message">Message from which the fields should be extracted</param>
  150. /// <returns>An instance of <see cref="MessageFields"/></returns>
  151. private MessageFields ExtractFields(string message)
  152. {
  153. var allFields = Parser.ExtractAllFields(message).Select(parser.MakeTyped).ToList();
  154. if (!allFields.Any())
  155. {
  156. log.TraceEvent(TraceEventType.Warning, 601, "Couldn't extract any fields from message: {0}", message);
  157. }
  158. else if (allFields.Count < 5)
  159. {
  160. log.TraceEvent(
  161. TraceEventType.Warning,
  162. 602,
  163. "Suspiciously low number of fields extracted ({0}) from the message: {1}",
  164. allFields.Count,
  165. message);
  166. }
  167. return new MessageFields(allFields);
  168. }
  169. /// <summary>
  170. /// Retrieves the message specification from the specification data container.
  171. /// </summary>
  172. /// <param name="msgType">Message type for which the spec should be retrieved</param>
  173. /// <exception cref="VerificationPreconditionException">Thrown when the verification options are set, but the message specification is missing.</exception>
  174. /// <returns>An instance of <see cref="SpecElement"/> or <c>null</c>, if no specification for message is available.</returns>
  175. private SpecElement GetSpecification(MsgType msgType)
  176. {
  177. SpecElement messageSpec;
  178. if (!specificationData.Messages.TryGetValue(msgType, out messageSpec) && options != VerificationOptions.None)
  179. {
  180. throw new VerificationPreconditionException(
  181. $"Verification options are set, but message with MsgType '{msgType.Value}' is not listed in the spec file. Either add it to the XML specification file, or turn off the message verification by setting VerificationOptions to null.");
  182. }
  183. return messageSpec;
  184. }
  185. /// <summary>
  186. /// Uses the <see cref="options"/> to verify the fields and message using the message specification.
  187. /// </summary>
  188. /// <param name="allFields">All fields in the message</param>
  189. /// <param name="message">The message itself</param>
  190. /// <param name="messageSpec">Specification for the message</param>
  191. /// <exception cref="MessageVerificationException">Thrown when the message does not conform to the specification.</exception>
  192. private void Verify(IList<Field> allFields, Message message, SpecElement messageSpec)
  193. {
  194. if (options.HasFlag(VerificationOptions.CheckRequiredFields))
  195. {
  196. foreach (var requiredHeaderTag in specificationData.Header?.RequiredTags ?? Enumerable.Empty<int>())
  197. {
  198. if (!message.Header.Contains(requiredHeaderTag))
  199. {
  200. throw new MessageVerificationException(
  201. $"Required tag {requiredHeaderTag} is missing from the header.");
  202. }
  203. }
  204. foreach (var requiredTrailerTag in specificationData.Trailer?.RequiredTags ?? Enumerable.Empty<int>())
  205. {
  206. if (!message.Trailer.Contains(requiredTrailerTag))
  207. {
  208. throw new MessageVerificationException(
  209. $"Required tag {requiredTrailerTag} is missing from the trailer.");
  210. }
  211. }
  212. foreach (var requiredBodyTag in messageSpec?.RequiredTags ?? Enumerable.Empty<int>())
  213. {
  214. if (!message.Contains(requiredBodyTag))
  215. {
  216. throw new MessageVerificationException(
  217. $"Required tag {requiredBodyTag} is missing from the message body.");
  218. }
  219. }
  220. }
  221. if (options.HasFlag(VerificationOptions.CheckHeaderFieldOrder))
  222. {
  223. foreach (
  224. var tuple in
  225. allFields.Zip(message.Header.OrderedFieldTags,
  226. (field, i) => new ValueTuple<int, int>(field.Tag, i)))
  227. {
  228. if (tuple.First != tuple.Second)
  229. {
  230. throw new MessageVerificationException(
  231. $"Header tags out of order: expected tag {tuple.Second}, got {tuple.First}.");
  232. }
  233. }
  234. }
  235. }
  236. }
  237. }