diff --git a/.github/workflows/build-status.yml b/.github/workflows/build-status.yml index 0134f2771..60868795a 100644 --- a/.github/workflows/build-status.yml +++ b/.github/workflows/build-status.yml @@ -3,8 +3,7 @@ on: push: branches: - master -env: - DOTNET_VERSION: '6.0.x' + jobs: build-and-test: name: Build And Test ${{matrix.os}} @@ -18,7 +17,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.DOTNET_VERSION }} + global-json-file: global.json - name: Restore nHapi (Windows) if: matrix.os == 'windows-latest' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 77bf197ef..3870ea71e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -83,4 +83,4 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 with: - category: "/language:${{matrix.language}}" + category: "/language:${{matrix.language}}" \ No newline at end of file diff --git a/.github/workflows/receive-pr.yml b/.github/workflows/receive-pr.yml index 184d92d26..db0187b65 100644 --- a/.github/workflows/receive-pr.yml +++ b/.github/workflows/receive-pr.yml @@ -1,8 +1,7 @@ name: Receive Pull Request on: [pull_request] -env: - DOTNET_VERSION: '6.0.x' + jobs: build-and-test: name: Build And Test ${{matrix.os}} @@ -16,7 +15,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.DOTNET_VERSION }} + global-json-file: global.json - name: Restore nHapi (Windows) if: matrix.os == 'windows-latest' diff --git a/global.json b/global.json index f7d2c85a5..55083d59a 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.101", + "version": "6.0.404", "rollForward": "latestFeature" }, "projects": [] diff --git a/src/NHapi.Base/Model/AbstractGroup.cs b/src/NHapi.Base/Model/AbstractGroup.cs index 719335acf..aba079eaf 100644 --- a/src/NHapi.Base/Model/AbstractGroup.cs +++ b/src/NHapi.Base/Model/AbstractGroup.cs @@ -42,16 +42,11 @@ namespace NHapi.Base.Model /// public abstract class AbstractGroup : IGroup { - private static readonly IHapiLog Log; + private static readonly IHapiLog Log = HapiLogFactory.GetHapiLog(typeof(AbstractGroup)); private readonly IModelClassFactory myFactory; private List items; - static AbstractGroup() - { - Log = HapiLogFactory.GetHapiLog(typeof(AbstractGroup)); - } - /// This constructor should be used by implementing classes that do not /// also implement Message. /// @@ -527,7 +522,7 @@ private IStructure TryToInstantiateStructure(Type c, string name) var argClasses = new[] { typeof(IGroup), typeof(IModelClassFactory) }; var argObjects = new object[] { this, myFactory }; var con = c.GetConstructor(argClasses); - o = con.Invoke(argObjects); + o = con!.Invoke(argObjects); } catch (MethodAccessException) { diff --git a/src/NHapi.Base/Model/AbstractPrimitive.cs b/src/NHapi.Base/Model/AbstractPrimitive.cs index 41f27c06a..2ecc36c03 100644 --- a/src/NHapi.Base/Model/AbstractPrimitive.cs +++ b/src/NHapi.Base/Model/AbstractPrimitive.cs @@ -56,10 +56,10 @@ public AbstractPrimitive(IMessage message, string description) { } - /// Sets the value of this Primitive, first performing validation as specified - /// by. getMessage().getValidationContext(). No validation is performed - /// if getMessage() returns null. - /// + /// + /// Sets the value of this Primitive, first performing validation as specified + /// by. Message.ValidationContext + /// No validation is performed if returns null. /// public virtual string Value { diff --git a/src/NHapi.Base/Model/GenericComposite.cs b/src/NHapi.Base/Model/GenericComposite.cs index d5e4b2e49..387fb5622 100644 --- a/src/NHapi.Base/Model/GenericComposite.cs +++ b/src/NHapi.Base/Model/GenericComposite.cs @@ -1,6 +1,7 @@ namespace NHapi.Base.Model { using System.Collections; + using System.Collections.Generic; /// /// An unspecified Composite datatype that has an undefined number of components, each @@ -11,7 +12,7 @@ namespace NHapi.Base.Model /// Bryan Tripp. public class GenericComposite : AbstractType, IComposite { - private readonly ArrayList components; + private readonly List components; /// /// Creates a new instance of GenericComposite. @@ -30,25 +31,13 @@ public GenericComposite(IMessage theMessage) public GenericComposite(IMessage theMessage, string description) : base(theMessage, description) { - components = new ArrayList(20); + components = new List(20); } /// /// Returns an array containing the components of this field. /// - public virtual IType[] Components - { - get - { - var ret = new IType[components.Count]; - for (var i = 0; i < ret.Length; i++) - { - ret[i] = (IType)components[i]; - } - - return ret; - } - } + public virtual IType[] Components => components.ToArray(); /// /// Returns the name of the type (used in XML encoding and profile checking). @@ -68,7 +57,7 @@ public virtual IType this[int index] components.Add(new Varies(Message)); } - return (IType)components[index]; + return components[index]; } } } diff --git a/src/NHapi.Base/Model/Primitive/CommonTM.cs b/src/NHapi.Base/Model/Primitive/CommonTM.cs index a5c92cd67..5436ba83a 100644 --- a/src/NHapi.Base/Model/Primitive/CommonTM.cs +++ b/src/NHapi.Base/Model/Primitive/CommonTM.cs @@ -151,15 +151,11 @@ public virtual string Value { // check to see if any of the following characters exist: "." or "+/-" // this will help us determine the acceptable lengths - var d = value.IndexOf("."); - var sp = value.IndexOf("+"); - var sm = value.IndexOf("-"); + var d = value.IndexOf(".", StringComparison.Ordinal); + var sp = value.IndexOf("+", StringComparison.Ordinal); + var sm = value.IndexOf("-", StringComparison.Ordinal); var indexOfSign = -1; - var offsetExists = false; - if ((sp != -1) || (sm != -1)) - { - offsetExists = true; - } + var offsetExists = (sp != -1) || (sm != -1); if (sp != -1) { @@ -189,7 +185,7 @@ public virtual string Value { // The length of the GMT offset must be 5 characters (including the sign) var msg = "The length of the TM datatype value does not conform to an allowable" + - " format. Format should conform to HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]"; + " format. Format should conform to HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]"; var e = new DataTypeException(msg); throw e; } @@ -201,7 +197,7 @@ public virtual string Value if ((timeVal.Length < 8) || (timeVal.Length > 11)) { var msg = "The length of the TM datatype value does not conform to an allowable" + - " format. Format should conform to HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]"; + " format. Format should conform to HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]"; var e = new DataTypeException(msg); throw e; } @@ -214,7 +210,7 @@ public virtual string Value if ((timeVal.Length != 2) && (timeVal.Length != 4) && (timeVal.Length != 6)) { var msg = "The length of the TM datatype value does not conform to an allowable" + - " format. Format should conform to HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]"; + " format. Format should conform to HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]"; var e = new DataTypeException(msg); throw e; } diff --git a/src/NHapi.Base/Model/Primitive/CommonTS.cs b/src/NHapi.Base/Model/Primitive/CommonTS.cs index 80b2748a7..81d8bd846 100644 --- a/src/NHapi.Base/Model/Primitive/CommonTS.cs +++ b/src/NHapi.Base/Model/Primitive/CommonTS.cs @@ -206,8 +206,8 @@ public virtual string Value string dateVal = null; string timeVal = null; string timeValLessOffset = null; - var sp = value.IndexOf("+"); - var sm = value.IndexOf("-"); + var sp = value.IndexOf("+", StringComparison.Ordinal); + var sm = value.IndexOf("-", StringComparison.Ordinal); var indexOfSign = -1; var offsetExists = false; var timeValIsOffsetOnly = false; @@ -310,7 +310,7 @@ public virtual string Value tm = new CommonTM(); // first extract the + sign from the offset value string if it exists - if (timeVal.IndexOf("+") == 0) + if (timeVal.IndexOf("+", StringComparison.Ordinal) == 0) { timeVal = timeVal.Substring(1); } // end if diff --git a/src/NHapi.Base/NHapi.Base.csproj b/src/NHapi.Base/NHapi.Base.csproj index 9d34b349c..455c9c6c0 100644 --- a/src/NHapi.Base/NHapi.Base.csproj +++ b/src/NHapi.Base/NHapi.Base.csproj @@ -14,6 +14,7 @@ + @@ -43,4 +44,4 @@ - + \ No newline at end of file diff --git a/src/NHapi.Base/Parser/DefaultModelClassFactory.cs b/src/NHapi.Base/Parser/DefaultModelClassFactory.cs index d9fca43f5..529621c1b 100644 --- a/src/NHapi.Base/Parser/DefaultModelClassFactory.cs +++ b/src/NHapi.Base/Parser/DefaultModelClassFactory.cs @@ -22,15 +22,10 @@ namespace NHapi.Base.Parser public class DefaultModelClassFactory : IModelClassFactory { private static readonly object LockObject = new object(); - private static readonly IHapiLog Log; + private static readonly IHapiLog Log = HapiLogFactory.GetHapiLog(typeof(DefaultModelClassFactory)); private static Hashtable packages = null; private static bool isLoadingPackages = false; - static DefaultModelClassFactory() - { - Log = HapiLogFactory.GetHapiLog(typeof(DefaultModelClassFactory)); - } - /// /// Lists all the packages (user-definable) where classes for standard and custom /// messages may be found. Each package has sub-packages called "message", diff --git a/src/NHapi.Base/Parser/DefaultXMLParser.cs b/src/NHapi.Base/Parser/DefaultXMLParser.cs index 7441fe731..02c2d4169 100644 --- a/src/NHapi.Base/Parser/DefaultXMLParser.cs +++ b/src/NHapi.Base/Parser/DefaultXMLParser.cs @@ -1,7 +1,7 @@ namespace NHapi.Base.Parser { using System; - using System.Collections; + using System.Collections.Generic; using System.IO; using System.Text; using System.Xml; @@ -29,12 +29,9 @@ namespace NHapi.Base.Parser /// Bryan Tripp. public class DefaultXMLParser : XMLParser { - private static readonly IHapiLog Log; + private static readonly IHapiLog Log = HapiLogFactory.GetHapiLog(typeof(DefaultXMLParser)); - static DefaultXMLParser() - { - Log = HapiLogFactory.GetHapiLog(typeof(DefaultXMLParser)); - } + private static readonly HashSet ForceGroupNames = new HashSet { "DIET" }; /// Creates a new instance of DefaultXMLParser. public DefaultXMLParser() @@ -63,30 +60,35 @@ public DefaultXMLParser(IModelClassFactory modelClassFactory) var messageFile = new FileInfo(args[0]); var fileLength = SupportClass.FileLength(messageFile); var r = new StreamReader(messageFile.FullName, Encoding.Default); - var cbuf = new char[(int)fileLength]; - Console.Out.WriteLine("Reading message file ... " + r.Read((char[])cbuf, 0, cbuf.Length) + " of " + fileLength + - " chars"); + var buffer = new char[fileLength]; + Console.Out.WriteLine( + $"Reading message file ... {r.Read(buffer, 0, buffer.Length)} of {fileLength} chars"); r.Close(); - var messString = Convert.ToString(cbuf); - - ParserBase inParser = null; - ParserBase outParser = null; - var pp = new PipeParser(); - XMLParser xp = new DefaultXMLParser(); - Console.Out.WriteLine("Encoding: " + pp.GetEncoding(messString)); - if (pp.GetEncoding(messString) != null) + var messString = Convert.ToString(buffer); + + ParserBase inParser; + ParserBase outParser; + var pipeParser = new PipeParser(); + XMLParser xmlParser = new DefaultXMLParser(); + Console.Out.WriteLine($"Encoding: {pipeParser.GetEncoding(messString)}"); + + if (EncodingDetector.IsEr7Encoded(messString)) + { + inParser = pipeParser; + outParser = xmlParser; + } + else if (EncodingDetector.IsXmlEncoded(messString)) { - inParser = pp; - outParser = xp; + inParser = xmlParser; + outParser = pipeParser; } - else if (xp.GetEncoding(messString) != null) + else { - inParser = xp; - outParser = pp; + throw new HL7Exception("Message encoding is not recognized"); } var mess = inParser.Parse(messString); - Console.Out.WriteLine("Got message of type " + mess.GetType().FullName); + Console.Out.WriteLine($"Got message of type {mess.GetType().FullName}"); var otherEncoding = outParser.Encode(mess); Console.Out.WriteLine(otherEncoding); @@ -97,71 +99,60 @@ public DefaultXMLParser(IModelClassFactory modelClassFactory) } } - ///

Creates an XML Document that corresponds to the given Message object.

- ///

If you are implementing this method, you should create an XML Document, and insert XML Elements - /// into it that correspond to the groups and segments that belong to the message type that your subclass - /// of XMLParser supports. Then, for each segment in the message, call the method - /// encode(Segment segmentObject, Element segmentElement) using the Element for - /// that segment and the corresponding Segment object from the given Message.

- ///
- public override XmlDocument EncodeDocument(IMessage source) + /// + public override XmlDocument EncodeDocument(IMessage source, ParserOptions parserOptions) { var messageClassName = source.GetType().FullName; - var messageName = messageClassName.Substring(messageClassName.LastIndexOf('.') + 1); - XmlDocument doc = null; + var messageName = messageClassName!.Substring(messageClassName.LastIndexOf('.') + 1); + + if (source is GenericMessage) + { + messageName = messageName.Replace("+", string.Empty); + } + + XmlDocument doc; try { doc = new XmlDocument(); - var root = doc.CreateElement(messageName); + var root = doc.CreateElement(messageName, NameSpace); doc.AppendChild(root); } catch (Exception e) { throw new HL7Exception( - "Can't create XML document - " + e.GetType().FullName, + $"Can't create XML document - {e.GetType().FullName}", ErrorCode.APPLICATION_INTERNAL_ERROR, e); } - Encode(source, (XmlElement)doc.DocumentElement); + Encode(source, doc.DocumentElement, parserOptions); return doc; } - ///

Creates and populates a Message object from an XML Document that contains an XML-encoded HL7 message.

- ///

The easiest way to implement this method for a particular message structure is as follows: - ///

  1. Create an instance of the Message type you are going to handle with your subclass - /// of XMLParser
  2. - ///
  3. Go through the given Document and find the Elements that represent the top level of - /// each message segment.
  4. - ///
  5. For each of these segments, call parse(Segment segmentObject, Element segmentElement), - /// providing the appropriate Segment from your Message object, and the corresponding Element.
- /// At the end of this process, your Message object should be populated with data from the XML - /// Document.

- ///
- /// HL7Exception if the message is not correctly formatted. - /// EncodingNotSupportedException if the message encoded. - /// is not supported by this parser. - /// + /// public override IMessage ParseDocument(XmlDocument xmlMessage, string version, ParserOptions parserOptions) { - if (parserOptions is null) + if (xmlMessage is null) { - throw new ArgumentNullException(nameof(parserOptions)); + throw new ArgumentNullException(nameof(xmlMessage)); } - var messageName = xmlMessage.DocumentElement.Name; + parserOptions ??= DefaultParserOptions; + + AssertNamespaceUri(xmlMessage.DocumentElement!.NamespaceURI); + + var messageName = xmlMessage.DocumentElement!.LocalName; var message = InstantiateMessage(messageName, version, true); Parse(message, xmlMessage.DocumentElement, parserOptions); return message; } + // TODO: should this be public? + // https://github.com/nHapiNET/nHapi/issues/399 /// public override void Parse(IMessage message, string @string, ParserOptions parserOptions) { - if (parserOptions is null) - { - throw new ArgumentNullException(nameof(parserOptions)); - } + parserOptions ??= DefaultParserOptions; try { @@ -186,9 +177,9 @@ public override void Parse(IMessage message, string @string, ParserOptions parse ///
protected internal static string MakeGroupElementName(string messageName, string className) { - string ret = null; + string ret; - if (className.Length > 4) + if (className.Length > 4 || ForceGroupNames.Contains(className)) { var elementName = new StringBuilder(); elementName.Append(messageName); @@ -217,74 +208,92 @@ private void Parse(IGroup groupObject, XmlElement groupElement, ParserOptions pa var messageName = groupObject.Message.GetStructureName(); var allChildNodes = groupElement.ChildNodes; - var unparsedElementList = new ArrayList(); + var unparsedElementList = new List(); for (var i = 0; i < allChildNodes.Count; i++) { - var node = allChildNodes.Item(i); - var name = node.Name; - if (Convert.ToInt16(node.NodeType) == (short)XmlNodeType.Element && !unparsedElementList.Contains(name)) + var node = allChildNodes[i]; + var name = node.LocalName; + if (node.NodeType == XmlNodeType.Element && !unparsedElementList.Contains(name)) { + AssertNamespaceUri(node.NamespaceURI); unparsedElementList.Add(name); } } // we're not too fussy about order here (all occurrences get parsed as repetitions) ... - for (var i = 0; i < childNames.Length; i++) + foreach (var nextChildName in childNames) { - SupportClass.ICollectionSupport.Remove(unparsedElementList, childNames[i]); - ParseReps(groupElement, groupObject, messageName, childNames[i], childNames[i], parserOptions); + var childName = nextChildName; + if (groupObject.IsGroup(nextChildName)) + { + childName = MakeGroupElementName(groupObject.Message.GetStructureName(), nextChildName); + } + + unparsedElementList.Remove(childName); + + // 4 char segment names are second occurrences of a segment within a single message + // structure. e.g. the second PID segment in an A17 patient swap message is known + // to nhapi's code representation as PID2 + if (nextChildName.Length == 4 && char.IsDigit(nextChildName[3])) + { + Log.Trace($"Skipping rep segment: {nextChildName}"); + } + else + { + ParseReps(groupElement, groupObject, messageName, nextChildName, nextChildName, parserOptions); + } } - for (var i = 0; i < unparsedElementList.Count; i++) + foreach (var segmentName in unparsedElementList) { - var segName = (string)unparsedElementList[i]; - var segIndexName = groupObject.AddNonstandardSegment(segName); - ParseReps(groupElement, groupObject, messageName, segName, segIndexName, parserOptions); + var segmentIndexName = groupObject.AddNonstandardSegment(segmentName); + ParseReps(groupElement, groupObject, messageName, segmentName, segmentIndexName, parserOptions); } } - /// Copies data from a group object into the corresponding group element, creating any - /// necessary child nodes. + /// + /// Copies data from a object into the corresponding , + /// creating any necessary child nodes. /// - private void Encode(IGroup groupObject, XmlElement groupElement) + /// The to encode. + /// The to encode into. + /// Contains configuration that will be applied when encoding. + /// If unable to encode . + private void Encode(IGroup groupObject, XmlElement groupElement, ParserOptions parserOptions) { var childNames = groupObject.Names; var messageName = groupObject.Message.GetStructureName(); try { - for (var i = 0; i < childNames.Length; i++) + foreach (var name in childNames) { - var reps = groupObject.GetAll(childNames[i]); - for (var j = 0; j < reps.Length; j++) + var reps = groupObject.GetAll(name); + foreach (var rep in reps) { + var elementName = MakeGroupElementName(messageName, name); var childElement = - groupElement.OwnerDocument.CreateElement(MakeGroupElementName(messageName, childNames[i])); - var hasValue = false; + groupElement.OwnerDocument!.CreateElement(elementName, NameSpace); - if (reps[j] is IGroup) - { - hasValue = true; - Encode((IGroup)reps[j], childElement); - } - else if (reps[j] is ISegment) + groupElement.AppendChild(childElement); + + if (rep is IGroup group) { - hasValue = Encode((ISegment)reps[j], childElement); + Encode(group, childElement, parserOptions); } - - if (hasValue) + else if (rep is ISegment segment) { - groupElement.AppendChild(childElement); + Encode(segment, childElement, parserOptions); } } } } - catch (Exception e) + catch (Exception ex) { throw new HL7Exception( "Can't encode group " + groupObject.GetType().FullName, ErrorCode.APPLICATION_INTERNAL_ERROR, - e); + ex); } } @@ -297,29 +306,45 @@ private void ParseReps( string childIndexName, ParserOptions parserOptions) { - var reps = GetChildElementsByTagName(groupElement, MakeGroupElementName(messageName, childName)); - Log.Debug("# of elements matching " + MakeGroupElementName(messageName, childName) + ": " + reps.Count); + var groupElementName = MakeGroupElementName(messageName, childName); + var reps = GetChildElementsByTagName(groupElement, groupElementName); + Log.Debug($"# of elements matching {MakeGroupElementName(messageName, childName)}: {reps.Count}"); if (groupObject.IsRepeating(childIndexName)) { for (var i = 0; i < reps.Count; i++) { - ParseRep((XmlElement)reps[i], groupObject.GetStructure(childIndexName, i), parserOptions); + ParseRep(reps[i], groupObject.GetStructure(childIndexName, i), parserOptions); } } else { if (reps.Count > 0) { - ParseRep((XmlElement)reps[0], groupObject.GetStructure(childIndexName, 0), parserOptions); + ParseRep(reps[0], groupObject.GetStructure(childIndexName, 0), parserOptions); } if (reps.Count > 1) { - var newIndexName = groupObject.AddNonstandardSegment(childName); - for (var i = 1; i < reps.Count; i++) + string newIndexName; + var i = 1; + try { - ParseRep((XmlElement)reps[i], groupObject.GetStructure(newIndexName, i - 1), parserOptions); + for (i = 1; i < reps.Count; i++) + { + newIndexName = childName + (i + 1); + var structure = groupObject.GetStructure(newIndexName); + ParseRep(reps[i], structure, parserOptions); + } + } + catch (Exception ex) + { + Log.Info("Issue Parsing", ex); + newIndexName = groupObject.AddNonstandardSegment(childName); + for (var j = i; j < reps.Count; j++) + { + ParseRep(reps[i], groupObject.GetStructure(newIndexName, j - i), parserOptions); + } } } } @@ -327,31 +352,34 @@ private void ParseReps( private void ParseRep(XmlElement theElem, IStructure theObj, ParserOptions parserOptions) { - if (theObj is IGroup) + if (theObj is IGroup group) { - Parse((IGroup)theObj, theElem, parserOptions); + Parse(group, theElem, parserOptions); } - else if (theObj is ISegment) + else if (theObj is ISegment segment) { - Parse((ISegment)theObj, theElem, parserOptions); + Parse(segment, theElem, parserOptions); } Log.Debug("Parsed element: " + theElem.Name); } // includes direct children only - private IList GetChildElementsByTagName(XmlElement theElement, string theName) + private IList GetChildElementsByTagName(XmlElement theElement, string theName) { - IList result = new ArrayList(10); + var result = new List(10); var children = theElement.ChildNodes; for (var i = 0; i < children.Count; i++) { - var child = children.Item(i); - if (Convert.ToInt16(child.NodeType) == (short)XmlNodeType.Element && child.Name.Equals(theName)) + var child = children[i]; + if (child.NodeType != XmlNodeType.Element || !child.LocalName.Equals(theName, StringComparison.Ordinal)) { - result.Add(child); + continue; } + + AssertNamespaceUri(child.NamespaceURI); + result.Add((XmlElement)child); } return result; diff --git a/src/NHapi.Base/Parser/LegacyDefaultXMLParser.cs b/src/NHapi.Base/Parser/LegacyDefaultXMLParser.cs new file mode 100644 index 000000000..4bc5d3120 --- /dev/null +++ b/src/NHapi.Base/Parser/LegacyDefaultXMLParser.cs @@ -0,0 +1,357 @@ +namespace NHapi.Base.Parser +{ + using System; + using System.Collections; + using System.IO; + using System.Text; + using System.Xml; + + using NHapi.Base.Log; + using NHapi.Base.Model; + + /// + /// A default XMLParser. This class assigns segment elements (in an XML-encoded message) + /// to Segment objects (in a Message object) using the name of a segment and the names + /// of any groups in which the segment is nested. The names of group classes must correspond + /// to the names of group elements (they must be identical except that a dot in the element + /// name, following the message name, is replaced with an underscore, in order to constitute a + /// valid class name). + /// + /// + /// At the time of writing, the group names in the XML spec are changing. Many of the group + /// names have been automatically generated based on the group contents. However, these automatic + /// names are gradually being replaced with manually assigned names. This process is expected to + /// be complete by November 2002. As a result, mismatches are likely. Messages could be + /// transformed prior to parsing (using XSLT) as a work-around. Alternatively the group class names + /// could be changed to reflect updates in the XML spec. Ultimately, HAPI group classes will be + /// changed to correspond with the official group names, once these are all assigned. + /// + /// Bryan Tripp. + [Obsolete("Use DefaultXMLParser instead.")] + public class LegacyDefaultXMLParser : LegacyXMLParser + { + private static readonly IHapiLog Log = HapiLogFactory.GetHapiLog(typeof(LegacyDefaultXMLParser)); + + /// Creates a new instance of DefaultXMLParser. + public LegacyDefaultXMLParser() + { + } + + /// Creates a new instance of DefaultXMLParser. + public LegacyDefaultXMLParser(IModelClassFactory modelClassFactory) + : base(modelClassFactory) + { + } + + /// Test harness. + [STAThread] + public static void Main(string[] args) + { + if (args.Length != 1) + { + Console.Out.WriteLine("Usage: DefaultXMLParser pipe_encoded_file"); + Environment.Exit(1); + } + + // read and parse message from file + try + { + var messageFile = new FileInfo(args[0]); + var fileLength = SupportClass.FileLength(messageFile); + var r = new StreamReader(messageFile.FullName, Encoding.Default); + var buffer = new char[(int)fileLength]; + Console.Out.WriteLine( + $"Reading message file ... {r.Read(buffer, 0, buffer.Length)} of {fileLength} chars"); + r.Close(); + var messString = Convert.ToString(buffer); + + ParserBase inParser; + ParserBase outParser; + var pp = new PipeParser(); + XMLParser xp = new DefaultXMLParser(); + Console.Out.WriteLine("Encoding: " + pp.GetEncoding(messString)); + if (pp.GetEncoding(messString) != null) + { + inParser = pp; + outParser = xp; + } + else if (xp.GetEncoding(messString) != null) + { + inParser = xp; + outParser = pp; + } + else + { + throw new HL7Exception("Message encoding is not recognized"); + } + + var mess = inParser.Parse(messString); + Console.Out.WriteLine("Got message of type " + mess.GetType().FullName); + + var otherEncoding = outParser.Encode(mess); + Console.Out.WriteLine(otherEncoding); + } + catch (Exception e) + { + SupportClass.WriteStackTrace(e, Console.Error); + } + } + + ///

Creates an XML Document that corresponds to the given Message object.

+ ///

If you are implementing this method, you should create an XML Document, and insert XML Elements + /// into it that correspond to the groups and segments that belong to the message type that your subclass + /// of XMLParser supports. Then, for each segment in the message, call the method + /// encode(Segment segmentObject, Element segmentElement) using the Element for + /// that segment and the corresponding Segment object from the given Message.

+ ///
+ public override XmlDocument EncodeDocument(IMessage source, ParserOptions parserOptions) + { + var messageClassName = source.GetType().FullName; + var messageName = messageClassName!.Substring(messageClassName.LastIndexOf('.') + 1); + XmlDocument doc; + try + { + doc = new XmlDocument(); + var root = doc.CreateElement(messageName); + doc.AppendChild(root); + } + catch (Exception e) + { + throw new HL7Exception( + "Can't create XML document - " + e.GetType().FullName, + ErrorCode.APPLICATION_INTERNAL_ERROR, + e); + } + + Encode(source, doc.DocumentElement, parserOptions); + return doc; + } + + ///

Creates and populates a Message object from an XML Document that contains an XML-encoded HL7 message.

+ ///

The easiest way to implement this method for a particular message structure is as follows: + ///

  1. Create an instance of the Message type you are going to handle with your subclass + /// of XMLParser
  2. + ///
  3. Go through the given Document and find the Elements that represent the top level of + /// each message segment.
  4. + ///
  5. For each of these segments, call parse(Segment segmentObject, Element segmentElement), + /// providing the appropriate Segment from your Message object, and the corresponding Element.
+ /// At the end of this process, your Message object should be populated with data from the XML + /// Document.

+ ///
+ /// HL7Exception if the message is not correctly formatted. + /// EncodingNotSupportedException if the message encoded. + /// is not supported by this parser. + /// + public override IMessage ParseDocument(XmlDocument xmlMessage, string version, ParserOptions parserOptions) + { + if (parserOptions is null) + { + throw new ArgumentNullException(nameof(parserOptions)); + } + + var messageName = xmlMessage.DocumentElement.Name; + var message = InstantiateMessage(messageName, version, true); + Parse(message, xmlMessage.DocumentElement, parserOptions); + return message; + } + + /// + public override void Parse(IMessage message, string @string, ParserOptions parserOptions) + { + parserOptions ??= DefaultParserOptions; + + try + { + var xmlDocument = new XmlDocument(); + xmlDocument.Load(new StringReader(@string)); + + Parse(message, xmlDocument.DocumentElement, parserOptions); + } + catch (XmlException e) + { + throw new HL7Exception("XmlException parsing XML", ErrorCode.APPLICATION_INTERNAL_ERROR, e); + } + } + + /// + /// Given the name of a message and a Group class, returns the corresponding group element name in an + /// XML-encoded message. This is the message name and group name separated by a dot. For example, + /// ADT_A01.INSURANCE. + /// + /// If it looks like a segment name (ie: has 3 characters), no change is made. + /// + /// + protected internal static string MakeGroupElementName(string messageName, string className) + { + string ret; + + if (className.Length > 4) + { + var elementName = new StringBuilder(); + elementName.Append(messageName); + elementName.Append('.'); + elementName.Append(className); + ret = elementName.ToString(); + } + else if (className.Length == 4) + { + ret = className.Substring(0, 3 - 0); + } + else + { + ret = className; + } + + return ret; + } + + /// Populates the given group object with data from the given group element, ignoring + /// any unrecognized nodes. + /// + private void Parse(IGroup groupObject, XmlElement groupElement, ParserOptions parserOptions) + { + var childNames = groupObject.Names; + var messageName = groupObject.Message.GetStructureName(); + + var allChildNodes = groupElement.ChildNodes; + var unparsedElementList = new ArrayList(); + for (var i = 0; i < allChildNodes.Count; i++) + { + var node = allChildNodes.Item(i); + var name = node.Name; + if (Convert.ToInt16(node.NodeType) == (short)XmlNodeType.Element && !unparsedElementList.Contains(name)) + { + unparsedElementList.Add(name); + } + } + + // we're not too fussy about order here (all occurrences get parsed as repetitions) ... + for (var i = 0; i < childNames.Length; i++) + { + SupportClass.ICollectionSupport.Remove(unparsedElementList, childNames[i]); + ParseReps(groupElement, groupObject, messageName, childNames[i], childNames[i], parserOptions); + } + + for (var i = 0; i < unparsedElementList.Count; i++) + { + var segName = (string)unparsedElementList[i]; + var segIndexName = groupObject.AddNonstandardSegment(segName); + ParseReps(groupElement, groupObject, messageName, segName, segIndexName, parserOptions); + } + } + + /// Copies data from a group object into the corresponding group element, creating any + /// necessary child nodes. + /// + private void Encode(IGroup groupObject, XmlElement groupElement, ParserOptions parserOptions) + { + var childNames = groupObject.Names; + var messageName = groupObject.Message.GetStructureName(); + + try + { + for (var i = 0; i < childNames.Length; i++) + { + var reps = groupObject.GetAll(childNames[i]); + for (var j = 0; j < reps.Length; j++) + { + var childElement = + groupElement.OwnerDocument.CreateElement(MakeGroupElementName(messageName, childNames[i])); + var hasValue = false; + + if (reps[j] is IGroup) + { + hasValue = true; + Encode((IGroup)reps[j], childElement, parserOptions); + } + else if (reps[j] is ISegment) + { + hasValue = Encode((ISegment)reps[j], childElement, parserOptions); + } + + if (hasValue) + { + groupElement.AppendChild(childElement); + } + } + } + } + catch (Exception e) + { + throw new HL7Exception( + "Can't encode group " + groupObject.GetType().FullName, + ErrorCode.APPLICATION_INTERNAL_ERROR, + e); + } + } + + // param childIndexName may have an integer on the end if >1 sibling with same name (e.g. NTE2) + private void ParseReps( + XmlElement groupElement, + IGroup groupObject, + string messageName, + string childName, + string childIndexName, + ParserOptions parserOptions) + { + var reps = GetChildElementsByTagName(groupElement, MakeGroupElementName(messageName, childName)); + Log.Debug("# of elements matching " + MakeGroupElementName(messageName, childName) + ": " + reps.Count); + + if (groupObject.IsRepeating(childIndexName)) + { + for (var i = 0; i < reps.Count; i++) + { + ParseRep((XmlElement)reps[i], groupObject.GetStructure(childIndexName, i), parserOptions); + } + } + else + { + if (reps.Count > 0) + { + ParseRep((XmlElement)reps[0], groupObject.GetStructure(childIndexName, 0), parserOptions); + } + + if (reps.Count > 1) + { + var newIndexName = groupObject.AddNonstandardSegment(childName); + for (var i = 1; i < reps.Count; i++) + { + ParseRep((XmlElement)reps[i], groupObject.GetStructure(newIndexName, i - 1), parserOptions); + } + } + } + } + + private void ParseRep(XmlElement theElem, IStructure theObj, ParserOptions parserOptions) + { + if (theObj is IGroup obj) + { + Parse(obj, theElem, parserOptions); + } + else if (theObj is ISegment segment) + { + Parse(segment, theElem, parserOptions); + } + + Log.Debug("Parsed element: " + theElem.Name); + } + + // includes direct children only + private IList GetChildElementsByTagName(XmlElement theElement, string theName) + { + IList result = new ArrayList(10); + var children = theElement.ChildNodes; + + for (var i = 0; i < children.Count; i++) + { + var child = children.Item(i); + if (Convert.ToInt16(child.NodeType) == (short)XmlNodeType.Element && child.Name.Equals(theName)) + { + result.Add(child); + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/NHapi.Base/Parser/LegacyPipeParser.cs b/src/NHapi.Base/Parser/LegacyPipeParser.cs index e84820528..3e8b2f198 100644 --- a/src/NHapi.Base/Parser/LegacyPipeParser.cs +++ b/src/NHapi.Base/Parser/LegacyPipeParser.cs @@ -50,12 +50,7 @@ public class LegacyPipeParser : ParserBase { private const string SegDelim = "\r"; // see section 2.8 of spec - private static readonly IHapiLog Log; - - static LegacyPipeParser() - { - Log = HapiLogFactory.GetHapiLog(typeof(LegacyPipeParser)); - } + private static readonly IHapiLog Log = HapiLogFactory.GetHapiLog(typeof(LegacyPipeParser)); /// Creates a new LegacyPipeParser. public LegacyPipeParser() @@ -257,12 +252,11 @@ public static string StripLeadingWhitespace(string in_Renamed) return out_Renamed.ToString(); } + // TODO: should this be public? + // https://github.com/nHapiNET/nHapi/issues/399 public override void Parse(IMessage message, string @string, ParserOptions parserOptions) { - if (parserOptions is null) - { - throw new ArgumentNullException(nameof(parserOptions)); - } + parserOptions ??= DefaultParserOptions; var messageIter = new Util.MessageIterator(message, "MSH", true); FilterIterator.IPredicate segmentsOnly = new IsSegmentPredicate(this); @@ -361,7 +355,7 @@ public override string GetEncoding(string message) var nextFieldDelimLoc = 0; for (var i = 0; i < 11; i++) { - nextFieldDelimLoc = message.IndexOf((char)fourthChar, nextFieldDelimLoc + 1); + nextFieldDelimLoc = message.IndexOf(fourthChar, nextFieldDelimLoc + 1); if (nextFieldDelimLoc < 0) { return null; @@ -378,8 +372,7 @@ public override string GetEncoding(string message) /// this method should not be public. /// - /// - /// + /// /// /// /// HL7Exception. @@ -389,28 +382,32 @@ public virtual string GetMessageStructure(string message) return GetStructure(message).Structure; } + // TODO: should this be public? + // https://github.com/nHapiNET/nHapi/issues/399 /// /// Parses a segment string and populates the given Segment object. Unexpected fields are /// added as Varies' at the end of the segment. /// - /// HL7Exception if the given string does not contain the. - /// given segment or if the string is not encoded properly. - /// + /// + /// If the given string does not contain the given segment or if the string is not encoded properly. + /// public virtual void Parse(ISegment destination, string segment, EncodingCharacters encodingChars) { Parse(destination, segment, encodingChars, DefaultParserOptions); } + // TODO: should this be public? + // https://github.com/nHapiNET/nHapi/issues/399 /// /// Parses a segment string and populates the given Segment object. Unexpected fields are /// added as Varies' at the end of the segment. /// - /// HL7Exception if the given string does not contain the. - /// given segment or if the string is not encoded properly. - /// + /// + /// If the given string does not contain the given segment or if the string is not encoded properly. + /// public virtual void Parse(ISegment destination, string segment, EncodingCharacters encodingChars, ParserOptions parserOptions) { - parserOptions = parserOptions ?? DefaultParserOptions; + parserOptions ??= DefaultParserOptions; var fieldOffset = 0; if (IsDelimDefSegment(destination.GetStructureName())) @@ -468,7 +465,7 @@ public virtual void Parse(ISegment destination, string segment, EncodingCharacte } // set data type of OBX-5 - if (destination.GetType().FullName.IndexOf("OBX") >= 0) + if (destination.GetType().FullName.IndexOf("OBX", StringComparison.Ordinal) >= 0) { Varies.FixOBX5(destination, Factory, parserOptions); } @@ -493,7 +490,7 @@ public virtual void Parse(ISegment destination, string segment, EncodingCharacte public override ISegment GetCriticalResponseData(string message) { // try to get MSH segment - var locStartMSH = message.IndexOf("MSH"); + var locStartMSH = message.IndexOf("MSH", StringComparison.Ordinal); if (locStartMSH < 0) { throw new HL7Exception("Couldn't find MSH segment in message: " + message, ErrorCode.SEGMENT_SEQUENCE_ERROR); @@ -561,14 +558,14 @@ public override ISegment GetCriticalResponseData(string message) public override string GetAckID(string message) { string ackID = null; - var startMSA = message.IndexOf("\rMSA"); + var startMSA = message.IndexOf("\rMSA", StringComparison.Ordinal); if (startMSA >= 0) { var startFieldOne = startMSA + 5; var fieldDelim = message[startFieldOne - 1]; - var start = message.IndexOf((char)fieldDelim, startFieldOne) + 1; - var end = message.IndexOf((char)fieldDelim, start); - var segEnd = message.IndexOf(Convert.ToString(SegDelim), start); + var start = message.IndexOf(fieldDelim, startFieldOne) + 1; + var end = message.IndexOf(fieldDelim, start); + var segEnd = message.IndexOf(Convert.ToString(SegDelim), start, StringComparison.Ordinal); if (segEnd > start && segEnd < end) { end = segEnd; @@ -597,20 +594,16 @@ public override string GetAckID(string message) return ackID; } - /// Returns the version ID (MSH-12) from the given message, without fully parsing the message. - /// The version is needed prior to parsing in order to determine the message class - /// into which the text of the message should be parsed. - /// - /// HL7Exception if the version field can not be found. - public override string GetVersion(string message) + /// + public override string GetVersion(string message, ParserOptions parserOptions) { - var startMSH = message.IndexOf("MSH"); + var startMSH = message.IndexOf("MSH", StringComparison.Ordinal); if (startMSH < 0) { throw new HL7Exception("No MSH header segment found.", ErrorCode.REQUIRED_FIELD_MISSING); } - var endMSH = message.IndexOf(SegDelim, startMSH); + var endMSH = message.IndexOf(SegDelim, startMSH, StringComparison.Ordinal); if (endMSH < 0) { endMSH = message.Length; @@ -672,14 +665,14 @@ public override string GetVersion(string message) /// EncodingNotSupportedException if the requested encoding is not. /// supported by this parser. /// - protected internal override string DoEncode(IMessage source, string encoding) + protected internal override string DoEncode(IMessage source, string encoding, ParserOptions parserOptions) { if (!SupportsEncoding(encoding)) { throw new EncodingNotSupportedException("This parser does not support the " + encoding + " encoding"); } - return Encode(source); + return Encode(source, parserOptions); } /// Formats a Message object into an HL7 message string using this parser's @@ -688,7 +681,7 @@ protected internal override string DoEncode(IMessage source, string encoding) /// HL7Exception if the data fields in the message do not permit encoding. /// (e.g. required fields are null). /// - protected internal override string DoEncode(IMessage source) + protected internal override string DoEncode(IMessage source, ParserOptions parserOptions) { // get encoding characters ... var msh = (ISegment)source.GetStructure("MSH"); @@ -729,7 +722,7 @@ protected internal override string DoEncode(IMessage source) { // Create the MsgType and Trigger Event if not there var messageTypeFullname = source.GetStructureName(); - var i = messageTypeFullname.IndexOf("_"); + var i = messageTypeFullname.IndexOf("_", StringComparison.Ordinal); if (i > 0) { var type = messageTypeFullname.Substring(0, i); @@ -753,7 +746,7 @@ protected internal override string DoEncode(IMessage source) var en = new EncodingCharacters(fieldSep, encCharString); // pass down to group encoding method which will operate recursively on children ... - return Encode((IGroup)source, en); + return Encode(source, en); } /// Parses a message string and returns the corresponding Message @@ -785,7 +778,7 @@ private static EncodingCharacters GetEncodingChars(string message) private static string EncodePrimitive(IPrimitive p, EncodingCharacters encodingChars) { - var val = ((IPrimitive)p).Value; + var val = p.Value; if (val == null) { val = string.Empty; @@ -834,11 +827,7 @@ private static string StripExtraDelimiters(string in_Renamed, char delim) /// private static bool IsDelimDefSegment(string theSegmentName) { - var is_Renamed = false; - if (theSegmentName.Equals("MSH") || theSegmentName.Equals("FHS") || theSegmentName.Equals("BHS")) - { - is_Renamed = true; - } + bool is_Renamed = theSegmentName.Equals("MSH") || theSegmentName.Equals("FHS") || theSegmentName.Equals("BHS"); return is_Renamed; } @@ -880,7 +869,7 @@ private MessageStructure GetStructure(string message) try { var fields = Split( - message.Substring(0, Math.Max(message.IndexOf(SegDelim), message.Length) - 0), + message.Substring(0, Math.Max(message.IndexOf(SegDelim, StringComparison.Ordinal), message.Length) - 0), Convert.ToString(ec.FieldSeparator)); wholeFieldNine = fields[8]; diff --git a/src/NHapi.Base/Parser/LegacyXMLParser.cs b/src/NHapi.Base/Parser/LegacyXMLParser.cs new file mode 100644 index 000000000..f98fa5472 --- /dev/null +++ b/src/NHapi.Base/Parser/LegacyXMLParser.cs @@ -0,0 +1,763 @@ +/* + The contents of this file are subject to the Mozilla Public License Version 1.1 + (the "License"); you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.mozilla.org/MPL/ + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the + specific language governing rights and limitations under the License. + The Original Code is "XMLParser.java". Description: + "Parses and encodes HL7 messages in XML form, according to HL7's normative XML encoding + specification." + The Initial Developer of the Original Code is University Health Network. Copyright (C) + 2002. All Rights Reserved. + Contributor(s): ______________________________________. + Alternatively, the contents of this file may be used under the terms of the + GNU General Public License (the "GPL"), in which case the provisions of the GPL are + applicable instead of those above. If you wish to allow use of your version of this + file only under the terms of the GPL and not to allow others to use your version + of this file under the MPL, indicate your decision by deleting the provisions above + and replace them with the notice and other provisions required by the GPL License. + If you do not delete the provisions above, a recipient may use your version of + this file under either the MPL or the GPL. +*/ + +namespace NHapi.Base.Parser +{ + using System; + using System.IO; + using System.Text; + using System.Xml; + + using NHapi.Base.Log; + using NHapi.Base.Model; + using NHapi.Base.Util; + + /// + /// Parses and encodes HL7 messages in XML form, according to HL7's normative XML encoding + /// specification. This is an abstract class that handles datatype and segment parsing/encoding, + /// but not the parsing/encoding of entire messages. To use the XML parser, you should create a + /// subclass for a certain message structure. This subclass must be able to identify the Segment + /// objects that correspond to various Segment nodes in an XML document, and call the methods. + /// parse(Segment segment, ElementNode segmentNode) and. encode(Segment segment, ElementNode segmentNode) + /// as appropriate. XMLParser uses the Xerces parser, which must be installed in your class path. + /// + /// Bryan Tripp, Shawn Bellina. + [Obsolete("Use XMLParser instead.")] + public abstract class LegacyXMLParser : ParserBase + { + private static readonly IHapiLog Log = HapiLogFactory.GetHapiLog(typeof(LegacyXMLParser)); + + /// + /// The nodes whose names match these strings will be kept as original, + /// meaning that no white space trimming will occur on them. + /// + private string[] keepAsOriginalNodes; + + /// All keepAsOriginalNodes names, concatenated by a pipe (|). + private string concatKeepAsOriginalNodes = string.Empty; + + protected LegacyXMLParser() + { + } + + protected LegacyXMLParser(IModelClassFactory factory) + : base(factory) + { + } + + /// + /// Gets the preferred encoding of this Parser. + /// + public override string DefaultEncoding => "XML"; + + /// + /// Sets the keepAsOriginalNodes. + /// + /// The nodes whose names match the keepAsOriginalNodes will be kept as original, + /// meaning that no white space trimming will occur on them. + /// + /// + public virtual string[] KeepAsOriginalNodes + { + get + { + return keepAsOriginalNodes; + } + + set + { + keepAsOriginalNodes = value; + + if (value.Length != 0) + { + // initializes the + var strBuf = new StringBuilder(value[0]); + for (var i = 1; i < value.Length; i++) + { + strBuf.Append("|"); + strBuf.Append(value[i]); + } + + concatKeepAsOriginalNodes = strBuf.ToString(); + } + else + { + concatKeepAsOriginalNodes = string.Empty; + } + } + } + + /// + /// Returns a String representing the encoding of the given message, if + /// the encoding is recognized. For example if the given message appears + /// to be encoded using HL7 2.x XML rules then "XML" would be returned. + /// If the encoding is not recognized then null is returned. That this + /// method returns a specific encoding does not guarantee that the + /// message is correctly encoded (e.g. well formed XML) - just that + /// it is not encoded using any other encoding than the one returned. + /// Returns null if the encoding is not recognized. + /// + public override string GetEncoding(string message) + { + string encoding = null; + + // check for a number of expected strings + var expected = new[] { "" }; + var isXML = true; + for (var i = 0; i < expected.Length; i++) + { + if (message.IndexOf(expected[i], StringComparison.Ordinal) < 0) + { + isXML = false; + } + } + + if (isXML) + { + encoding = "XML"; + } + + return encoding; + } + + ///

Creates and populates a Message object from an XML Document that contains an XML-encoded HL7 message.

+ ///

The easiest way to implement this method for a particular message structure is as follows: + ///

  1. Create an instance of the Message type you are going to handle with your subclass + /// of XMLParser
  2. + ///
  3. Go through the given Document and find the Elements that represent the top level of + /// each message segment.
  4. + ///
  5. For each of these segments, call parse(Segment segmentObject, Element segmentElement), + /// providing the appropriate Segment from your Message object, and the corresponding Element.
+ /// At the end of this process, your Message object should be populated with data from the XML + /// Document.

+ ///
+ /// HL7Exception if the message is not correctly formatted. + /// EncodingNotSupportedException if the message encoded. + /// is not supported by this parser. + /// + public IMessage ParseDocument(XmlDocument xmlMessage, string version) + { + return ParseDocument(xmlMessage, version, DefaultParserOptions); + } + + ///

Creates and populates a Message object from an XML Document that contains an XML-encoded HL7 message.

+ ///

The easiest way to implement this method for a particular message structure is as follows: + ///

  1. Create an instance of the Message type you are going to handle with your subclass + /// of XMLParser
  2. + ///
  3. Go through the given Document and find the Elements that represent the top level of + /// each message segment.
  4. + ///
  5. For each of these segments, call parse(Segment segmentObject, Element segmentElement), + /// providing the appropriate Segment from your Message object, and the corresponding Element.
+ /// At the end of this process, your Message object should be populated with data from the XML + /// Document.

+ ///
+ /// HL7Exception if the message is not correctly formatted. + /// EncodingNotSupportedException if the message encoded. + /// is not supported by this parser. + /// + public abstract IMessage ParseDocument(XmlDocument xmlMessage, string version, ParserOptions parserOptions); + + ///

Creates an XML Document that corresponds to the given Message object.

+ ///

If you are implementing this method, you should create an XML Document, and insert XML Elements + /// into it that correspond to the groups and segments that belong to the message type that your subclass + /// of XMLParser supports. Then, for each segment in the message, call the method + /// encode(Segment segmentObject, Element segmentElement) using the Element for + /// that segment and the corresponding Segment object from the given Message.

+ ///
+ public abstract XmlDocument EncodeDocument(IMessage source, ParserOptions parserOptions); + + public XmlDocument EncodeDocument(IMessage source) + { + return EncodeDocument(source, DefaultParserOptions); + } + + /// Populates the given Segment object with data from the given XML Element. + /// HL7Exception if the XML Element does not have the correct name and structure. + /// for the given Segment, or if there is an error while setting individual field values. + /// + public virtual void Parse(ISegment segmentObject, XmlElement segmentElement) + { + Parse(segmentObject, segmentElement, DefaultParserOptions); + } + + /// Populates the given Segment object with data from the given XML Element. + /// HL7Exception if the XML Element does not have the correct name and structure. + /// for the given Segment, or if there is an error while setting individual field values. + /// + public virtual void Parse(ISegment segmentObject, XmlElement segmentElement, ParserOptions parserOptions) + { + parserOptions ??= DefaultParserOptions; + + var done = new SupportClass.HashSetSupport(); + var all = segmentElement.ChildNodes; + for (var i = 0; i < all.Count; i++) + { + var elementName = all.Item(i).Name; + if (Convert.ToInt16(all.Item(i).NodeType) == (short)XmlNodeType.Element && !done.Contains(elementName)) + { + done.Add(elementName); + + var index = elementName.IndexOf('.'); + if (index >= 0 && elementName.Length > index) + { + // properly formatted element + var fieldNumString = elementName.Substring(index + 1); + var fieldNum = int.Parse(fieldNumString); + ParseReps(segmentObject, segmentElement, elementName, fieldNum); + } + else + { + Log.Debug("Child of segment " + segmentObject.GetStructureName() + " doesn't look like a field: " + elementName); + } + } + } + + // set data type of OBX-5 + if (segmentObject.GetType().FullName.IndexOf("OBX", StringComparison.Ordinal) >= 0) + { + Varies.FixOBX5(segmentObject, Factory, parserOptions); + } + } + + /// Populates the given Element with data from the given Segment, by inserting + /// Elements corresponding to the Segment's fields, their components, etc. Returns + /// true if there is at least one data value in the segment. + /// + public virtual bool Encode(ISegment segmentObject, XmlElement segmentElement) + { + return Encode(segmentObject, segmentElement, DefaultParserOptions); + } + + /// Populates the given Element with data from the given Segment, by inserting + /// Elements corresponding to the Segment's fields, their components, etc. Returns + /// true if there is at least one data value in the segment. + /// + public virtual bool Encode(ISegment segmentObject, XmlElement segmentElement, ParserOptions parserOptions) + { + var hasValue = false; + var n = segmentObject.NumFields(); + for (var i = 1; i <= n; i++) + { + var name = MakeElementName(segmentObject, i); + var reps = segmentObject.GetField(i); + for (var j = 0; j < reps.Length; j++) + { + var newNode = segmentElement.OwnerDocument.CreateElement(name); + + var componentHasValue = Encode(reps[j], newNode, parserOptions); + if (componentHasValue) + { + try + { + segmentElement.AppendChild(newNode); + } + catch (Exception e) + { + throw new HL7Exception("DOMException encoding Segment: ", ErrorCode.APPLICATION_INTERNAL_ERROR, e); + } + + hasValue = true; + } + } + } + + return hasValue; + } + + /// Populates the given Type object with data from the given XML Element. + public virtual void Parse(IType datatypeObject, XmlElement datatypeElement) + { + // TODO: consider replacing with a switch statement + if (datatypeObject is Varies) + { + ParseVaries((Varies)datatypeObject, datatypeElement); + } + else if (datatypeObject is IPrimitive) + { + ParsePrimitive((IPrimitive)datatypeObject, datatypeElement); + } + else if (datatypeObject is IComposite) + { + ParseComposite((IComposite)datatypeObject, datatypeElement); + } + } + + ///

Returns a minimal amount of data from a message string, including only the + /// data needed to send a response to the remote system. This includes the + /// following fields: + /// + /// field separator + /// encoding characters + /// processing ID + /// message control ID + /// + /// This method is intended for use when there is an error parsing a message, + /// (so the Message object is unavailable) but an error message must be sent + /// back to the remote system including some of the information in the inbound + /// message. This method parses only that required information, hopefully + /// avoiding the condition that caused the original error.

+ ///
+ public override ISegment GetCriticalResponseData(string message) + { + var version = GetVersion(message); + var criticalData = MakeControlMSH(version, Factory); + + Terser.Set(criticalData, 1, 0, 1, 1, ParseLeaf(message, "MSH.1", 0)); + Terser.Set(criticalData, 2, 0, 1, 1, ParseLeaf(message, "MSH.2", 0)); + Terser.Set(criticalData, 10, 0, 1, 1, ParseLeaf(message, "MSH.10", 0)); + var procID = ParseLeaf(message, "MSH.11", 0); + if (procID == null || procID.Length == 0) + { + procID = ParseLeaf(message, "PT.1", message.IndexOf("MSH.11", StringComparison.Ordinal)); + + // this field is a composite in later versions + } + + Terser.Set(criticalData, 11, 0, 1, 1, procID); + + return criticalData; + } + + /// + /// For response messages, returns the value of MSA-2 (the message ID of the message + /// sent by the sending system). This value may be needed prior to main message parsing, + /// so that (particularly in a multi-threaded scenario) the message can be routed to + /// the thread that sent the request. We need this information first so that any + /// parse exceptions are thrown to the correct thread. Implementers of Parsers should + /// take care to make the implementation of this method very fast and robust. + /// Returns null if MSA-2 can not be found (e.g. if the message is not a + /// response message). Trims whitespace from around the MSA-2 field. + /// + public override string GetAckID(string message) + { + try + { + return ParseLeaf(message, "msa.2", 0).Trim(); + } + catch (HL7Exception) + { + // OK ... assume it isn't a response message + return null; + } + } + + /// + public override string GetVersion(string message, ParserOptions parserOptions) + { + var version = ParseLeaf(message, "MSH.12", 0); + if (version == null || version.Trim().Length == 0) + { + version = ParseLeaf(message, "VID.1", message.IndexOf("MSH.12", StringComparison.Ordinal)); + } + + return version; + } + + /// + /// Checks if a node content should be kept as original (ie.: whitespaces won't be removed). + /// + /// The target node. + /// + /// true if whitespaces should not be removed from node content; otherwise, false. + /// + protected internal virtual bool KeepAsOriginal(XmlNode node) + { + return + node.Name != null + && concatKeepAsOriginalNodes.IndexOf(node.Name, StringComparison.Ordinal) != -1; + } + + /// + /// Removes all unnecessary whitespace from the given String (intended to be used with Primitive values). + /// This includes leading and trailing whitespace, and repeated space characters. Carriage returns, + /// line feeds, and tabs are replaced with spaces. + /// + protected internal virtual string RemoveWhitespace(string s) + { + s = s.Replace('\r', ' '); + s = s.Replace('\n', ' '); + s = s.Replace('\t', ' '); + + var repeatedSpacesExist = true; + while (repeatedSpacesExist) + { + var loc = s.IndexOf(" ", StringComparison.Ordinal); + if (loc < 0) + { + repeatedSpacesExist = false; + } + else + { + var buf = new StringBuilder(); + buf.Append(s.Substring(0, loc - 0)); + buf.Append(" "); + buf.Append(s.Substring(loc + 2)); + s = buf.ToString(); + } + } + + return s.Trim(); + } + + /// + /// Parses a message string and returns the corresponding Message + /// object. This method checks that the given message string is XML encoded, creates an + /// XML Document object (using Xerces) from the given String, and calls the abstract + /// method . + /// + protected internal override IMessage DoParse(string message, string version, ParserOptions parserOptions) + { + IMessage m; + + // parse message string into a DOM document + try + { + var doc = new XmlDocument(); + doc.Load(new StringReader(message)); + + m = ParseDocument(doc, version, parserOptions); + } + catch (XmlException e) + { + throw new HL7Exception("XmlException parsing XML", ErrorCode.APPLICATION_INTERNAL_ERROR, e); + } + catch (IOException e) + { + throw new HL7Exception("IOException parsing XML", ErrorCode.APPLICATION_INTERNAL_ERROR, e); + } + + return m; + } + + /// + /// Formats a Message object into an HL7 message string using the given encoding. + /// + /// Thrown if the data fields in the message do not permit encoding (e.g. required fields are null). + /// Thrown if the requested encoding is not supported by this parser. + protected internal override string DoEncode(IMessage source, string encoding, ParserOptions parserOptions) + { + if (!SupportsEncoding("XML")) + { + throw new EncodingNotSupportedException("XMLParser supports only XML encoding"); + } + + return Encode(source, parserOptions); + } + + /// + /// Formats a Message object into an HL7 message string using this parser's + /// default encoding (XML encoding). This method calls the abstract method. + /// encodeDocument(...) in order to obtain XML Document object + /// representation of the Message, then serializes it to a String. + /// + /// Thrown if the data fields in the message do not permit encoding (e.g. required fields are null). + protected internal override string DoEncode(IMessage source, ParserOptions parserOptions) + { + if (source is GenericMessage) + { + throw new HL7Exception( + "Can't XML-encode a GenericMessage. Message must have a recognized structure."); + } + + var doc = EncodeDocument(source, parserOptions); + doc.DocumentElement.SetAttribute("xmlns", "urn:hl7-org:v2xml"); + + return doc.OuterXml; + } + + /// + /// Attempts to retrieve the value of a leaf tag without using DOM or SAX. + /// This method searches the given message string for the given tag name, and returns + /// everything after the given tag and before the start of the next tag. Whitespace + /// is stripped. This is intended only for lead nodes, as the value is considered to + /// end at the start of the next tag, regardless of whether it is the matching end + /// tag or some other nested tag. + /// + /// a string message in XML form. + /// the name of the XML tag, e.g. "MSA.2". + /// the character location at which to start searching. + /// Thrown if the tag can not be found. + protected internal virtual string ParseLeaf(string message, string tagName, int startAt) + { + var tagStart = message.IndexOf("<" + tagName, startAt, StringComparison.Ordinal); + if (tagStart < 0) + { + tagStart = message.IndexOf("<" + tagName.ToUpperInvariant(), startAt, StringComparison.Ordinal); + } + + var valStart = message.IndexOf(">", tagStart, StringComparison.Ordinal) + 1; + var valEnd = message.IndexOf("<", valStart, StringComparison.Ordinal); + + string value; + if (tagStart >= 0 && valEnd >= valStart) + { + value = message.Substring(valStart, valEnd - valStart); + } + else + { + throw new HL7Exception( + $"Couldn't find {tagName} in message beginning: {message.Substring(0, Math.Min(150, message.Length) - 0)}", + ErrorCode.REQUIRED_FIELD_MISSING); + } + + return value; + } + + /// Populates a Composite type by looping through it's children, finding corresponding + /// Elements among the children of the given Element, and calling parse(Type, Element) for + /// each. + /// + private void ParseComposite(IComposite datatypeObject, XmlElement datatypeElement) + { + if (datatypeObject is GenericComposite) + { + // elements won't be named GenericComposite.x + var children = datatypeElement.ChildNodes; + var compNum = 0; + for (var i = 0; i < children.Count; i++) + { + if (Convert.ToInt16(children.Item(i).NodeType) == (short)XmlNodeType.Element) + { + Parse(datatypeObject[compNum], (XmlElement)children.Item(i)); + compNum++; + } + } + } + else + { + var children = datatypeObject.Components; + for (var i = 0; i < children.Length; i++) + { + var matchingElements = datatypeElement.GetElementsByTagName(MakeElementName(datatypeObject, i + 1)); + if (matchingElements.Count > 0) + { + Parse(children[i], (XmlElement)matchingElements.Item(0)); // components don't repeat - use 1st + } + } + } + } + + /// + /// Returns the expected XML element name for the given child of the given Segment. + /// + private string MakeElementName(ISegment s, int child) + { + return $"{s.GetStructureName()}.{child}"; + } + + /// + /// Returns the expected XML element name for the given child of the given Composite. + /// + private string MakeElementName(IComposite composite, int child) + { + return $"{composite.TypeName}.{child}"; + } + + /// + /// Populates the given with data from the given , by inserting + /// XmlElements corresponding to the Type's components and values. + /// + /// + /// if the given type contains a value (i.e. for Primitives, if + /// doesn't return null, and for Composites, if at least one underlying + /// Primitive doesn't return null). + /// + private bool Encode(IType datatypeObject, XmlElement datatypeElement, ParserOptions parserOptions) + { + var hasData = false; + + // TODO: consider using a switch statement + if (datatypeObject is Varies) + { + hasData = EncodeVaries((Varies)datatypeObject, datatypeElement, parserOptions); + } + else if (datatypeObject is IPrimitive) + { + hasData = EncodePrimitive((IPrimitive)datatypeObject, datatypeElement); + } + else if (datatypeObject is IComposite) + { + hasData = EncodeComposite((IComposite)datatypeObject, datatypeElement, parserOptions); + } + + return hasData; + } + + /// + /// Encodes a Varies type by extracting it's data field and encoding that. + /// + /// if the data field (or one of its components) contains a value. + private bool EncodeVaries(Varies datatypeObject, XmlElement datatypeElement, ParserOptions parserOptions) + { + var hasData = false; + if (datatypeObject.Data != null) + { + hasData = Encode(datatypeObject.Data, datatypeElement, parserOptions); + } + + return hasData; + } + + /// + /// Encodes a Primitive in XML by adding it's value as a child of the given Element. + /// + /// if the given Primitive contains a value. + private bool EncodePrimitive(IPrimitive datatypeObject, XmlElement datatypeElement) + { + var hasValue = datatypeObject.Value != null && !datatypeObject.Value.Equals(string.Empty); + + var t = datatypeElement.OwnerDocument.CreateTextNode(datatypeObject.Value); + if (hasValue) + { + try + { + datatypeElement.AppendChild(t); + } + catch (Exception e) + { + throw new DataTypeException("DOMException encoding Primitive: ", e); + } + } + + return hasValue; + } + + /// + /// Encodes a Composite in XML by looping through it's components, creating new + /// children for each of them (with the appropriate names) and populating them by + /// calling using these children. + /// + /// if at least one component contains a value. + private bool EncodeComposite(IComposite datatypeObject, XmlElement datatypeElement, ParserOptions parserOptions) + { + var components = datatypeObject.Components; + var hasValue = false; + for (var i = 0; i < components.Length; i++) + { + var name = MakeElementName(datatypeObject, i + 1); + var newNode = datatypeElement.OwnerDocument.CreateElement(name); + var componentHasValue = Encode(components[i], newNode, parserOptions); + if (componentHasValue) + { + try + { + datatypeElement.AppendChild(newNode); + } + catch (Exception e) + { + throw new DataTypeException("DOMException encoding Composite: ", e); + } + + hasValue = true; + } + } + + return hasValue; + } + + private void ParseReps(ISegment segmentObject, XmlElement segmentElement, string fieldName, int fieldNum) + { + var reps = segmentElement.GetElementsByTagName(fieldName); + for (var i = 0; i < reps.Count; i++) + { + Parse(segmentObject.GetField(fieldNum, i), (XmlElement)reps.Item(i)); + } + } + + /// + /// Parses an XML element into a Varies by determining whether the element is primitive or + /// composite, calling setData() on the Varies with a new generic primitive or composite as appropriate, + /// and then calling parse again with the new Type object. + /// + private void ParseVaries(Varies datatypeObject, XmlElement datatypeElement) + { + // figure out what data type it holds + if (!HasChildElement(datatypeElement)) + { + // it's a primitive + datatypeObject.Data = new GenericPrimitive(datatypeObject.Message); + } + else + { + // it's a composite ... almost know what type, except that we don't have the version here + datatypeObject.Data = new GenericComposite(datatypeObject.Message); + } + + Parse(datatypeObject.Data, datatypeElement); + } + + /// Returns true if any of the given element's children are elements. + private bool HasChildElement(XmlElement e) + { + var children = e.ChildNodes; + var hasElement = false; + var c = 0; + while (c < children.Count && !hasElement) + { + if (Convert.ToInt16(children.Item(c).NodeType) == (short)XmlNodeType.Element) + { + hasElement = true; + } + + c++; + } + + return hasElement; + } + + /// Parses a primitive type by filling it with text child, if any. + private void ParsePrimitive(IPrimitive datatypeObject, XmlElement datatypeElement) + { + var children = datatypeElement.ChildNodes; + var c = 0; + var full = false; + while (c < children.Count && !full) + { + var child = children.Item(c++); + if (Convert.ToInt16(child.NodeType) == (short)XmlNodeType.Text) + { + try + { + if (child.Value != null && !child.Value.Equals(string.Empty)) + { + if (KeepAsOriginal(child.ParentNode)) + { + datatypeObject.Value = child.Value; + } + else + { + datatypeObject.Value = RemoveWhitespace(child.Value); + } + } + } + catch (Exception e) + { + Log.Error("Error parsing primitive value from TEXT_NODE", e); + } + + full = true; + } + } + } + } +} \ No newline at end of file diff --git a/src/NHapi.Base/Parser/ParserBase.cs b/src/NHapi.Base/Parser/ParserBase.cs index e7644044c..4b09258d3 100644 --- a/src/NHapi.Base/Parser/ParserBase.cs +++ b/src/NHapi.Base/Parser/ParserBase.cs @@ -45,16 +45,10 @@ namespace NHapi.Base.Parser public abstract class ParserBase { protected static readonly ParserOptions DefaultParserOptions = new ParserOptions(); - - private static readonly IHapiLog Log; + private static readonly IHapiLog Log = HapiLogFactory.GetHapiLog(typeof(ParserBase)); private IValidationContext validationContext; private MessageValidator messageValidator; - static ParserBase() - { - Log = HapiLogFactory.GetHapiLog(typeof(ParserBase)); - } - /// /// Uses DefaultModelClassFactory for model class lookup. /// @@ -82,10 +76,7 @@ protected ParserBase(IModelClassFactory theFactory) ///
public IValidationContext ValidationContext { - get - { - return validationContext; - } + get => validationContext; set { @@ -121,13 +112,13 @@ public static ISegment MakeControlMSH(string version, IModelClassFactory factory (IMessage)GenericMessage .GetGenericMessageClass(version) .GetConstructor(new[] { typeof(IModelClassFactory) }) - .Invoke(new object[] { factory }); + !.Invoke(new object[] { factory }); - var c = factory.GetSegmentClass("MSH", version); + var @class = factory.GetSegmentClass("MSH", version); var constructorParamTypes = new[] { typeof(IGroup), typeof(IModelClassFactory) }; var constructorParamArgs = new object[] { dummy, factory }; - var constructor = c.GetConstructor(constructorParamTypes); - msh = (ISegment)constructor.Invoke(constructorParamArgs); + var constructor = @class.GetConstructor(constructorParamTypes); + msh = (ISegment)constructor!.Invoke(constructorParamArgs); } catch (Exception e) { @@ -210,6 +201,8 @@ public virtual IMessage Parse(string message) /// If is null. public virtual IMessage Parse(string message, ParserOptions parserOptions) { + parserOptions ??= DefaultParserOptions; + var encoding = GetEncoding(message); if (!SupportsEncoding(encoding)) @@ -224,17 +217,14 @@ public virtual IMessage Parse(string message, ParserOptions parserOptions) } } - if (startOfMessage == null) - { - startOfMessage = message.Substring(0, Math.Min(message.Length, 50)); - } + startOfMessage ??= message.Substring(0, Math.Min(message.Length, 50)); throw new EncodingNotSupportedException( $"Determine encoding for message. The following is the first 50 chars of the message for reference, although this may not be where the issue is: {startOfMessage}"); } var version = GetVersion(message); - if (!ValidVersion(version)) + if (!parserOptions.AllowUnknownVersions && !ValidVersion(version)) { throw new HL7Exception( $"Can't process message of version '{version}' - version not recognized", @@ -284,6 +274,8 @@ public virtual IMessage Parse(string message, string version, ParserOptions pars return result; } + // TODO: should this be public? + // https://github.com/nHapiNET/nHapi/issues/399 /// /// Parses a particular message and returns the encoded structure. Uses the default /// . @@ -296,6 +288,8 @@ public void Parse(IMessage message, string @string) Parse(message, @string, DefaultParserOptions); } + // TODO: should this be public? + // https://github.com/nHapiNET/nHapi/issues/399 /// /// Parses a particular message and returns the encoded structure. /// @@ -303,7 +297,6 @@ public void Parse(IMessage message, string @string) /// The string to parse. /// Contains configuration that will be applied when parsing. /// If there is a problem encoding. - /// If is null. public abstract void Parse(IMessage message, string @string, ParserOptions parserOptions); /// @@ -317,9 +310,25 @@ public void Parse(IMessage message, string @string) /// Thrown if the data fields in the message do not permit encoding (e.g. required fields are null). /// Thrown if the requested encoding is not supported by this parser. public virtual string Encode(IMessage source, string encoding) + { + return Encode(source, encoding, DefaultParserOptions); + } + + /// + /// Formats a object into an HL7 message string using the given encoding. + /// + /// An object from which to construct an encoded message string. + /// the name of the HL7 encoding to use (eg "XML"; most implementations support only + /// one encoding). + /// + /// Contains configuration that will be applied when encoding. + /// The encoded message. + /// Thrown if the data fields in the message do not permit encoding (e.g. required fields are null). + /// Thrown if the requested encoding is not supported by this parser. + public virtual string Encode(IMessage source, string encoding, ParserOptions parserOptions) { messageValidator.Validate(source); - var result = DoEncode(source, encoding); + var result = DoEncode(source, encoding, parserOptions); messageValidator.Validate(result, encoding.Equals("XML"), source.Version); return result; @@ -334,11 +343,25 @@ public virtual string Encode(IMessage source, string encoding) /// /// The encoded message. public virtual string Encode(IMessage source) + { + return Encode(source, DefaultParserOptions); + } + + /// + /// Formats a Message object into an HL7 message string using this parsers + /// default encoding. + /// + /// + /// A Message object from which to construct an encoded message string. + /// + /// Contains configuration that will be applied when encoding. + /// The encoded message. + public virtual string Encode(IMessage source, ParserOptions parserOptions) { var encoding = DefaultEncoding; messageValidator.Validate(source); - var result = DoEncode(source); + var result = DoEncode(source, parserOptions); messageValidator.Validate(result, encoding.Equals("XML"), source.Version); return result; @@ -422,27 +445,43 @@ public bool SupportsEncoding(string encoding) /// The version is needed prior to parsing in order to determine the message class /// into which the text of the message should be parsed. /// + /// The message to inspect. + /// Thrown if the version field can not be found. + public virtual string GetVersion(string message) + { + return GetVersion(message, DefaultParserOptions); + } + + /// + /// Returns the version ID (MSH-12) from the given message, without fully parsing the message. + /// The version is needed prior to parsing in order to determine the message class + /// into which the text of the message should be parsed. + /// + /// The message to inspect. + /// Contains configuration that will be applied when parsing. /// Thrown if the version field can not be found. - public abstract string GetVersion(string message); + public abstract string GetVersion(string message, ParserOptions parserOptions); /// - /// Called by to perform implementation-specific encoding work. + /// Called by to perform implementation-specific encoding work. /// /// a Message object from which to construct an encoded message string. /// the name of the HL7 encoding to use (eg "XML"; most implementations support only one encoding). + /// Contains configuration that will be applied when encoding. /// The encoded message. /// Thrown if the data fields in the message do not permit encoding (e.g. required fields are null). /// Thrown if the requested encoding is not supported by this parser. - protected internal abstract string DoEncode(IMessage source, string encoding); + protected internal abstract string DoEncode(IMessage source, string encoding, ParserOptions parserOptions); /// - /// Called by to perform implementation-specific encoding work. + /// Called by to perform implementation-specific encoding work. /// /// a Message object from which to construct an encoded message string. + /// Contains configuration that will be applied when encoding. /// The encoded message. /// Thrown if the data fields in the message do not permit encoding (e.g. required fields are null). /// Thrown if the requested encoding is not supported by this parser. - protected internal abstract string DoEncode(IMessage source); + protected internal abstract string DoEncode(IMessage source, ParserOptions parserOptions); /// /// Called by to perform implementation-specific parsing work. @@ -492,9 +531,10 @@ protected internal virtual IMessage InstantiateMessage(string theName, string th } Log.Info($"Instantiating msg of class {messageClass.FullName}"); - var constructor = messageClass.GetConstructor(new Type[] { typeof(IModelClassFactory) }); - var result = (IMessage)constructor.Invoke(new object[] { Factory }); + var constructor = messageClass.GetConstructor(new[] { typeof(IModelClassFactory) }); + var result = (IMessage)constructor!.Invoke(new object[] { Factory }); result.ValidationContext = validationContext; + return result; } } diff --git a/src/NHapi.Base/Parser/ParserOptions.cs b/src/NHapi.Base/Parser/ParserOptions.cs index 0162273ce..57ea1e756 100644 --- a/src/NHapi.Base/Parser/ParserOptions.cs +++ b/src/NHapi.Base/Parser/ParserOptions.cs @@ -1,15 +1,36 @@ namespace NHapi.Base.Parser { + using System; + using System.Collections.Generic; + public class ParserOptions { public ParserOptions() { + AllowUnknownVersions = false; DefaultObx2Type = null; InvalidObx2Type = null; UnexpectedSegmentBehaviour = UnexpectedSegmentBehaviour.AddInline; NonGreedyMode = false; + DisableWhitespaceTrimmingOnAllXmlNodes = false; + XmlNodeNamesToDisableWhitespaceTrimming = new HashSet(StringComparer.Ordinal); + PrettyPrintEncodedXml = true; } + /// + /// + /// If set to , the parser will allow messages to parse, even if they + /// contain a version which is not known to the parser. + /// + /// + /// When operating in this mode, if a message arrives which an unknown version string, the + /// parser will attempt to parse it using a class + /// instead of a specific nhapi structure class. + /// + /// + /// The default value is . + public bool AllowUnknownVersions { get; set; } + /// /// If this property is set, the value provides a default datatype ("ST", /// "NM", etc) for an OBX segment with a missing OBX-2 value. This is useful @@ -57,11 +78,12 @@ public ParserOptions() public UnexpectedSegmentBehaviour UnexpectedSegmentBehaviour { get; set; } /// - /// If set to true (default is false), pipe parser will be put in non-greedy mode. This setting + /// If set to , pipe parser will be put in non-greedy mode. This setting /// applies only to and will have no effect on . /// /// /// + /// The default value is . /// /// In non-greedy mode, if the message structure being parsed has an ambiguous choice of where to put a segment /// because there is a segment matching the current segment name in both a later position in the message, and @@ -106,5 +128,39 @@ public ParserOptions() /// /// public bool NonGreedyMode { get; set; } + + /// + /// + /// If set to , the is configured to treat all whitespace + /// text nodes as literal, meaning that line breaks, tabs, multiple spaces, etc. will be preserved. + /// + /// + /// If set to , any values passed to + /// will be superseded since all whitespace will treated as literal. + /// + /// + /// The default value is . + public bool DisableWhitespaceTrimmingOnAllXmlNodes { get; set; } + + /// + /// + /// Configures the to treat all whitespace within the given + /// as literal, meaning that line breaks, tabs, multiple spaces, etc. will be preserved. + /// + /// + /// The default value is an Empty . + public HashSet XmlNodeNamesToDisableWhitespaceTrimming { get; set; } + + /// + /// + /// If set to , the will attempt to pretty-print the XML + /// they generate. + /// + /// + /// This means the messages will look nicer to humans, but may take up slightly more space/bandwidth. + /// + /// + /// The default value is . + public bool PrettyPrintEncodedXml { get; set; } } -} +} \ No newline at end of file diff --git a/src/NHapi.Base/Parser/PipeParser.cs b/src/NHapi.Base/Parser/PipeParser.cs index 8c2f4baf3..43edb2ce0 100644 --- a/src/NHapi.Base/Parser/PipeParser.cs +++ b/src/NHapi.Base/Parser/PipeParser.cs @@ -148,9 +148,8 @@ public static string[] Split(string composite, string delimiter) /// Thrown if the requested encoding is not supported by this parser. public static string Encode(IType source, EncodingCharacters encodingChars) { - if (source is Varies) + if (source is Varies varies) { - var varies = (Varies)source; if (varies.Data != null) { source = varies.Data; @@ -313,6 +312,8 @@ public virtual string GetMessageStructure(string message) return GetStructure(message).Structure; } + // TODO: should this be public? + // https://github.com/nHapiNET/nHapi/issues/399 /// /// Parses a segment string and populates the given Segment object. /// @@ -330,6 +331,8 @@ public virtual void Parse(ISegment destination, string segment, EncodingCharacte Parse(destination, segment, encodingChars, DefaultParserOptions); } + // TODO: should this be public? + // https://github.com/nHapiNET/nHapi/issues/399 /// /// Parses a segment string and populates the given Segment object. /// @@ -345,10 +348,12 @@ public virtual void Parse(ISegment destination, string segment, EncodingCharacte /// public virtual void Parse(ISegment destination, string segment, EncodingCharacters encodingChars, ParserOptions parserOptions) { - parserOptions = parserOptions ?? DefaultParserOptions; + parserOptions ??= DefaultParserOptions; Parse(destination, segment, encodingChars, 0, parserOptions); } + // TODO: should this be public? + // https://github.com/nHapiNET/nHapi/issues/399 /// /// Parses a segment string and populates the given Segment object. /// @@ -367,6 +372,8 @@ public virtual void Parse(ISegment destination, string segment, EncodingCharacte Parse(destination, segment, encodingChars, repetition, DefaultParserOptions); } + // TODO: should this be public? + // https://github.com/nHapiNET/nHapi/issues/399 /// /// Parses a segment string and populates the given Segment object. /// @@ -383,7 +390,7 @@ public virtual void Parse(ISegment destination, string segment, EncodingCharacte /// public virtual void Parse(ISegment destination, string segment, EncodingCharacters encodingChars, int repetition, ParserOptions parserOptions) { - parserOptions = parserOptions ?? DefaultParserOptions; + parserOptions ??= DefaultParserOptions; var fieldOffset = 0; if (IsDelimDefSegment(destination.GetStructureName())) @@ -445,15 +452,19 @@ public virtual void Parse(ISegment destination, string segment, EncodingCharacte } // set data type of OBX-5 - if (destination.GetType().FullName.IndexOf("OBX") >= 0) + if (destination.GetType().FullName!.IndexOf("OBX", StringComparison.Ordinal) >= 0) { Varies.FixOBX5(destination, Factory, parserOptions); } } + // TODO: should this be public? + // https://github.com/nHapiNET/nHapi/issues/399 /// public override void Parse(IMessage message, string @string, ParserOptions parserOptions) { + parserOptions ??= DefaultParserOptions; + if (parserOptions is null) { throw new ArgumentNullException(nameof(parserOptions)); @@ -539,7 +550,7 @@ public override void Parse(IMessage message, string @string, ParserOptions parse public override ISegment GetCriticalResponseData(string message) { // try to get MSH segment - var mshStart = message.IndexOf("MSH"); + var mshStart = message.IndexOf("MSH", StringComparison.Ordinal); if (mshStart < 0) { throw new HL7Exception("Couldn't find MSH segment in message: " + message, ErrorCode.SEGMENT_SEQUENCE_ERROR); @@ -600,7 +611,7 @@ public override ISegment GetCriticalResponseData(string message) public override string GetAckID(string message) { string ackID = null; - var msaStart = message.IndexOf("\rMSA"); + var msaStart = message.IndexOf("\rMSA", StringComparison.Ordinal); if (msaStart >= 0) { @@ -608,7 +619,7 @@ public override string GetAckID(string message) var fieldDelimiter = message[startFieldOne - 1]; var start = message.IndexOf(fieldDelimiter, startFieldOne) + 1; var end = message.IndexOf(fieldDelimiter, start); - var segEnd = message.IndexOf(SegmentDelimiter, start); + var segEnd = message.IndexOf(SegmentDelimiter, start, StringComparison.Ordinal); if (segEnd > start && segEnd < end) { @@ -639,10 +650,10 @@ public override string GetAckID(string message) } /// - public override string GetVersion(string message) + public override string GetVersion(string message, ParserOptions parserOptions) { - var startMsh = message.IndexOf("MSH"); - var endMsh = message.IndexOf(SegmentDelimiter, startMsh); + var startMsh = message.IndexOf("MSH", StringComparison.Ordinal); + var endMsh = message.IndexOf(SegmentDelimiter, startMsh, StringComparison.Ordinal); if (endMsh < 0) { @@ -670,7 +681,7 @@ public override string GetVersion(string message) else { throw new HL7Exception( - $"Can't find encoding characters - MSH has only {fields.Length} fields", + $"Can't find encoding characters - MSH has only {fields.Length} fields - MSH-2 is {fields[1]}", ErrorCode.REQUIRED_FIELD_MISSING); } @@ -689,22 +700,28 @@ public override string GetVersion(string message) ErrorCode.REQUIRED_FIELD_MISSING); } + if (parserOptions.AllowUnknownVersions) + { + // TODO: Version.HighestAvailableVersionOrDefault.Version + // https://github.com/nHapiNET/nHapi/issues/400 + } + return comp[0]; } /// - protected internal override string DoEncode(IMessage source, string encoding) + protected internal override string DoEncode(IMessage source, string encoding, ParserOptions parserOptions) { if (!SupportsEncoding(encoding)) { throw new EncodingNotSupportedException("This parser does not support the " + encoding + " encoding"); } - return Encode(source); + return Encode(source, parserOptions); } /// - protected internal override string DoEncode(IMessage source) + protected internal override string DoEncode(IMessage source, ParserOptions parserOptions) { // get encoding characters ... var msh = (ISegment)source.GetStructure("MSH"); @@ -736,7 +753,7 @@ protected internal override string DoEncode(IMessage source) { // Create the MsgType and Trigger Event if not there var messageTypeFullname = source.GetStructureName(); - var i = messageTypeFullname.IndexOf("_"); + var i = messageTypeFullname.IndexOf("_", StringComparison.Ordinal); if (i > 0) { var type = messageTypeFullname.Substring(0, i); @@ -874,9 +891,10 @@ private EncodingCharacters GetValidEncodingCharacters(char fieldSep, ISegment ms Terser.Set(msh, 2, 0, 1, 1, encCharString); } - var version27 = "2.7"; + var version27 = new Version("2.7"); + var messageVersion = new Version(msh.Message.Version); - if (string.CompareOrdinal(version27, msh.Message.Version) > 0 && encCharString.Length != 4) + if (version27 > messageVersion && encCharString.Length != 4) { throw new HL7Exception( $"Encoding characters (MSH-2) value '{encCharString}' invalid -- must be 4 characters", ErrorCode.DATA_TYPE_ERROR); @@ -904,7 +922,7 @@ private MessageStructure GetStructure(string message) try { var fields = Split( - message.Substring(0, Math.Max(message.IndexOf(SegmentDelimiter), message.Length) - 0), + message.Substring(0, Math.Max(message.IndexOf(SegmentDelimiter, StringComparison.Ordinal), message.Length) - 0), Convert.ToString(encodingCharacters.FieldSeparator)); wholeFieldNine = fields[8]; @@ -972,7 +990,7 @@ private IStructureDefinition GetStructureDefinition(IMessage theMessage) Log.Info($"Instantiating msg of class {messageType.FullName}"); var constructor = messageType.GetConstructor(new[] { typeof(IModelClassFactory) }); - var message = (IMessage)constructor.Invoke(new object[] { Factory }); + var message = (IMessage)constructor!.Invoke(new object[] { Factory }); StructureDefinition previousLeaf = null; retVal = CreateStructureDefinition(message, ref previousLeaf); diff --git a/src/NHapi.Base/Parser/XMLParser.cs b/src/NHapi.Base/Parser/XMLParser.cs index 4aec12f12..795500864 100644 --- a/src/NHapi.Base/Parser/XMLParser.cs +++ b/src/NHapi.Base/Parser/XMLParser.cs @@ -28,8 +28,11 @@ this file under either the MPL or the GPL. namespace NHapi.Base.Parser { using System; + using System.Collections.Generic; using System.IO; + using System.Linq; using System.Text; + using System.Text.RegularExpressions; using System.Xml; using NHapi.Base.Log; @@ -48,39 +51,23 @@ namespace NHapi.Base.Parser /// Bryan Tripp, Shawn Bellina. public abstract class XMLParser : ParserBase { - private static readonly IHapiLog Log; + protected static readonly string NameSpace = "urn:hl7-org:v2xml"; - private readonly XmlDocument parser; + private static readonly IHapiLog Log = HapiLogFactory.GetHapiLog(typeof(XMLParser)); - /// - /// The nodes whose names match these strings will be kept as original, - /// meaning that no white space trimming will occur on them. - /// - private string[] keepAsOriginalNodes; + private static readonly string EscapeAttrName = "V"; - /// All keepAsOriginalNodes names, concatenated by a pipe (|). - private string concatKeepAsOriginalNodes = string.Empty; + private static readonly string EscapeNodeName = "escape"; - static XMLParser() - { - Log = HapiLogFactory.GetHapiLog(typeof(XMLParser)); - } + private static readonly Regex NameSpaceRegex = new Regex(@$"xmlns(.*)=""{NameSpace}""", RegexOptions.Compiled); protected XMLParser() { - parser = new XmlDocument - { - PreserveWhitespace = false, - }; } protected XMLParser(IModelClassFactory factory) : base(factory) { - parser = new XmlDocument - { - PreserveWhitespace = false, - }; } /// @@ -89,41 +76,13 @@ protected XMLParser(IModelClassFactory factory) public override string DefaultEncoding => "XML"; /// - /// Sets the keepAsOriginalNodes. /// - /// The nodes whose names match the keepAsOriginalNodes will be kept as original, + /// The nodes whose names match these strings will be kept as original, /// meaning that no white space trimming will occur on them. /// /// - public virtual string[] KeepAsOriginalNodes - { - get - { - return keepAsOriginalNodes; - } - - set - { - keepAsOriginalNodes = value; - - if (value.Length != 0) - { - // initializes the - var strBuf = new StringBuilder(value[0]); - for (var i = 1; i < value.Length; i++) - { - strBuf.Append("|"); - strBuf.Append(value[i]); - } - - concatKeepAsOriginalNodes = strBuf.ToString(); - } - else - { - concatKeepAsOriginalNodes = string.Empty; - } - } - } + [Obsolete("This method has been replaced by 'ParserOptions.XmlNodeNamesToDisableWhitespaceTrimming'.")] + public virtual IEnumerable KeepAsOriginalNodes { get; set; } = new List(); /// Test harness. [STAThread] @@ -142,11 +101,11 @@ public static void Main(string[] args) var messageFile = new FileInfo(args[0]); var fileLength = SupportClass.FileLength(messageFile); var r = new StreamReader(messageFile.FullName, Encoding.Default); - var cbuf = new char[(int)fileLength]; + var buffer = new char[(int)fileLength]; Console.Out.WriteLine( - $"Reading message file ... {r.Read((char[])cbuf, 0, cbuf.Length)} of {fileLength} chars"); + $"Reading message file ... {r.Read(buffer, 0, buffer.Length)} of {fileLength} chars"); r.Close(); - var messString = Convert.ToString(cbuf); + var messString = Convert.ToString(buffer); var mess = parser.Parse(messString); Console.Out.WriteLine("Got message of type " + mess.GetType().FullName); @@ -162,26 +121,25 @@ public static void Main(string[] args) if (reps[j] is ISegment) { // ignore groups - var docBuilder = new XmlDocument(); var doc = new XmlDocument(); // new doc for each segment - var root = doc.CreateElement(reps[j].GetType().FullName); + var root = doc.CreateElement(reps[j].GetType().FullName, NameSpace); doc.AppendChild(root); xp.Encode((ISegment)reps[j], root); - var out_Renamed = new StringWriter(); + var outRenamed = new StringWriter(); Console.Out.WriteLine("Segment " + reps[j].GetType().FullName + ": \r\n" + doc.OuterXml); - var segmentConstructTypes = new Type[] { typeof(IMessage) }; + var segmentConstructTypes = new[] { typeof(IMessage) }; var segmentConstructArgs = new object[] { null }; var s = (ISegment)reps[j].GetType().GetConstructor(segmentConstructTypes).Invoke(segmentConstructArgs); xp.Parse(s, root); var doc2 = new XmlDocument(); - var root2 = doc2.CreateElement(s.GetType().FullName); + var root2 = doc2.CreateElement(s.GetType().FullName, NameSpace); doc2.AppendChild(root2); xp.Encode(s, root2); var out2 = new StringWriter(); var ser = XmlWriter.Create(out2); doc.WriteTo(ser); - if (out2.ToString().Equals(out_Renamed.ToString())) + if (out2.ToString().Equals(outRenamed.ToString())) { Console.Out.WriteLine("Re-encode OK"); } @@ -212,103 +170,138 @@ public static void Main(string[] args) /// public override string GetEncoding(string message) { - string encoding = null; - - // check for a number of expected strings - var expected = new string[] { "" }; - var isXML = true; - for (var i = 0; i < expected.Length; i++) - { - if (message.IndexOf(expected[i]) < 0) - { - isXML = false; - } - } - - if (isXML) - { - encoding = "XML"; - } - - return encoding; + return EncodingDetector.IsXmlEncoded(message) ? DefaultEncoding : null; } - ///

Creates and populates a Message object from an XML Document that contains an XML-encoded HL7 message.

- ///

The easiest way to implement this method for a particular message structure is as follows: - ///

  1. Create an instance of the Message type you are going to handle with your subclass - /// of XMLParser
  2. - ///
  3. Go through the given Document and find the Elements that represent the top level of - /// each message segment.
  4. - ///
  5. For each of these segments, call parse(Segment segmentObject, Element segmentElement), - /// providing the appropriate Segment from your Message object, and the corresponding Element.
- /// At the end of this process, your Message object should be populated with data from the XML - /// Document.

- ///
- /// HL7Exception if the message is not correctly formatted. - /// EncodingNotSupportedException if the message encoded. - /// is not supported by this parser. + /// + /// Creates and populates a Message object from an XML Document that contains an XML-encoded HL7 message. + /// + /// The easiest way to implement this method for a particular message structure is as follows: + /// + /// Create an instance of the Message type you are going to handle with your subclass of + /// + /// Go through the given and find the XmlElements + /// that represent the top level of each message segment. + /// For each of these segments, call , + /// providing the appropriate from your object, and the + /// corresponding . + /// + /// At the end of this process, your object should be populated with data from the + /// . + /// /// + /// Xml encoded HL7 parsed into . + /// The name of the HL7 version to which the message belongs (eg "2.5"). + /// A parsed HL7 message. + /// If the message is not correctly formatted. + /// + /// If the message encoded is not supported by this parser. + /// public IMessage ParseDocument(XmlDocument xmlMessage, string version) { return ParseDocument(xmlMessage, version, DefaultParserOptions); } - ///

Creates and populates a Message object from an XML Document that contains an XML-encoded HL7 message.

- ///

The easiest way to implement this method for a particular message structure is as follows: - ///

  1. Create an instance of the Message type you are going to handle with your subclass - /// of XMLParser
  2. - ///
  3. Go through the given Document and find the Elements that represent the top level of - /// each message segment.
  4. - ///
  5. For each of these segments, call parse(Segment segmentObject, Element segmentElement), - /// providing the appropriate Segment from your Message object, and the corresponding Element.
- /// At the end of this process, your Message object should be populated with data from the XML - /// Document.

- ///
- /// HL7Exception if the message is not correctly formatted. - /// EncodingNotSupportedException if the message encoded. - /// is not supported by this parser. + /// + /// Creates and populates a Message object from an XML Document that contains an XML-encoded HL7 message. + /// + /// The easiest way to implement this method for a particular message structure is as follows: + /// + /// Create an instance of the Message type you are going to handle with your subclass of + /// + /// Go through the given and find the XmlElements + /// that represent the top level of each message segment. + /// For each of these segments, call , + /// providing the appropriate from your object, and the + /// corresponding . + /// + /// At the end of this process, your object should be populated with data from the + /// . + /// /// + /// Xml encoded HL7 parsed into . + /// The name of the HL7 version to which the message belongs (eg "2.5"). + /// Contains configuration that will be applied when parsing. + /// A parsed HL7 message. + /// If the message is not correctly formatted. + /// + /// If the message encoded is not supported by this parser. + /// public abstract IMessage ParseDocument(XmlDocument xmlMessage, string version, ParserOptions parserOptions); - ///

Creates an XML Document that corresponds to the given Message object.

- ///

If you are implementing this method, you should create an XML Document, and insert XML Elements - /// into it that correspond to the groups and segments that belong to the message type that your subclass - /// of XMLParser supports. Then, for each segment in the message, call the method - /// encode(Segment segmentObject, Element segmentElement) using the Element for - /// that segment and the corresponding Segment object from the given Message.

+ /// + /// Creates an that corresponds to the given object. + /// If you are implementing this method, you should create an , and insert + /// XmlElements into it that correspond to the IGroups and + /// ISegments that belong to the type that your subclass + /// of supports. Then, for each segment in the message, call the method + /// using the for + /// that segment and the corresponding object from the given . + /// + /// An object from which to construct an encoded message string. + /// An representation of the HL7 message. + /// When unable to create/populate the . + public XmlDocument EncodeDocument(IMessage source) + { + return EncodeDocument(source, DefaultParserOptions); + } + + /// + /// Creates an that corresponds to the given object. + /// If you are implementing this method, you should create an , and insert + /// XmlElements into it that correspond to the IGroups and + /// ISegments that belong to the type that your subclass + /// of supports. Then, for each segment in the message, call the method + /// using the for + /// that segment and the corresponding object from the given . /// - public abstract XmlDocument EncodeDocument(IMessage source); + /// An object from which to construct an encoded message string. + /// Contains configuration that will be applied when encoding. + /// An representation of the HL7 message. + /// When unable to create/populate the . + public abstract XmlDocument EncodeDocument(IMessage source, ParserOptions parserOptions); - /// Populates the given Segment object with data from the given XML Element. - /// HL7Exception if the XML Element does not have the correct name and structure. - /// for the given Segment, or if there is an error while setting individual field values. + /// + /// Populates the given object with data from the given . /// + /// The to parse into. + /// The to be parse. + /// + /// If the does not have the correct name and structure for the given + /// , or if there is an error while setting individual field values. + /// public virtual void Parse(ISegment segmentObject, XmlElement segmentElement) { Parse(segmentObject, segmentElement, DefaultParserOptions); } - /// Populates the given Segment object with data from the given XML Element. - /// HL7Exception if the XML Element does not have the correct name and structure. - /// for the given Segment, or if there is an error while setting individual field values. + /// + /// Populates the given object with data from the given . /// + /// The to parse into. + /// The to be parse. + /// Contains configuration that will be applied when parsing. + /// + /// If the does not have the correct name and structure for the given + /// , or if there is an error while setting individual field values. + /// + /// + /// If any of the of have an invalid namespace. + /// public virtual void Parse(ISegment segmentObject, XmlElement segmentElement, ParserOptions parserOptions) { - parserOptions = parserOptions ?? DefaultParserOptions; + parserOptions ??= DefaultParserOptions; - var done = new SupportClass.HashSetSupport(); + var done = new HashSet(StringComparer.Ordinal); + var children = segmentElement.ChildNodes; - // for (int i = 1; i <= segmentObject.NumFields(); i++) { - // String elementName = makeElementName(segmentObject, i); - // done.add(elementName); - // parseReps(segmentObject, segmentElement, elementName, i); - // } - var all = segmentElement.ChildNodes; - for (var i = 0; i < all.Count; i++) + for (var i = 0; i < children.Count; i++) { - var elementName = all.Item(i).Name; - if (Convert.ToInt16(all.Item(i).NodeType) == (short)XmlNodeType.Element && !done.Contains(elementName)) + var elementName = children[i].LocalName; + if (children[i].NodeType == XmlNodeType.Element && !done.Contains(elementName)) { + AssertNamespaceUri(children[i].NamespaceURI); + done.Add(elementName); var index = elementName.IndexOf('.'); @@ -317,27 +310,50 @@ public virtual void Parse(ISegment segmentObject, XmlElement segmentElement, Par // properly formatted element var fieldNumString = elementName.Substring(index + 1); var fieldNum = int.Parse(fieldNumString); - ParseReps(segmentObject, segmentElement, elementName, fieldNum); + ParseReps(segmentObject, segmentElement, elementName, fieldNum, parserOptions); } else { - Log.Debug("Child of segment " + segmentObject.GetStructureName() + " doesn't look like a field: " + elementName); + Log.Debug( + $"Child of segment {segmentObject.GetStructureName()} doesn't look like a field: {elementName}"); } } } // set data type of OBX-5 - if (segmentObject.GetType().FullName.IndexOf("OBX") >= 0) + if (segmentObject.GetType().FullName.IndexOf("OBX", StringComparison.Ordinal) >= 0) { Varies.FixOBX5(segmentObject, Factory, parserOptions); } + + // TODO set data type of MFE-4 } - /// Populates the given Element with data from the given Segment, by inserting - /// Elements corresponding to the Segment's fields, their components, etc. Returns - /// true if there is at least one data value in the segment. + /// + /// Populates the given with data from the given , by inserting + /// XmlElements corresponding to the Segment's fields, + /// their components, etc. /// + /// The to be encoded. + /// The to encode into. + /// if there is at least one data value in the . + /// If an error occurred while encoding. public virtual bool Encode(ISegment segmentObject, XmlElement segmentElement) + { + return Encode(segmentObject, segmentElement, DefaultParserOptions); + } + + /// + /// Populates the given with data from the given , by inserting + /// XmlElements corresponding to the Segment's fields, + /// their components, etc. + /// + /// The to be encoded. + /// The to encode into. + /// Contains configuration that will be applied when encoding. + /// if there is at least one data value in the . + /// If an error occurred while encoding. + public virtual bool Encode(ISegment segmentObject, XmlElement segmentElement, ParserOptions parserOptions) { var hasValue = false; var n = segmentObject.NumFields(); @@ -347,46 +363,66 @@ public virtual bool Encode(ISegment segmentObject, XmlElement segmentElement) var reps = segmentObject.GetField(i); for (var j = 0; j < reps.Length; j++) { - var newNode = segmentElement.OwnerDocument.CreateElement(name); - var componentHasValue = Encode(reps[j], newNode); - if (componentHasValue) + var newNode = segmentElement.OwnerDocument.CreateElement(name, NameSpace); + + var componentHasValue = Encode(reps[j], newNode, parserOptions); + if (!componentHasValue) { - try - { - segmentElement.AppendChild(newNode); - } - catch (Exception e) - { - throw new HL7Exception("DOMException encoding Segment: ", ErrorCode.APPLICATION_INTERNAL_ERROR, e); - } + continue; + } - hasValue = true; + try + { + segmentElement.AppendChild(newNode); } + catch (Exception e) + { + throw new HL7Exception($"DOMException encoding Segment: ", ErrorCode.APPLICATION_INTERNAL_ERROR, e); + } + + hasValue = true; } } return hasValue; } - /// Populates the given Type object with data from the given XML Element. + /// + /// Populates the given object with data from the given . + /// + /// The to parse into. + /// The to be parsed. + /// if the data did not match the expected type rules. public virtual void Parse(IType datatypeObject, XmlElement datatypeElement) + { + Parse(datatypeObject, datatypeElement, DefaultParserOptions); + } + + /// + /// Populates the given object with data from the given . + /// + /// The to parse into. + /// The to be parsed. + /// Contains configuration that will be applied when parsing. + /// if the data did not match the expected type rules. + public virtual void Parse(IType datatypeObject, XmlElement datatypeElement, ParserOptions parserOptions) { // TODO: consider replacing with a switch statement - if (datatypeObject is Varies) + if (datatypeObject is Varies varies) { - ParseVaries((Varies)datatypeObject, datatypeElement); + ParseVaries(varies, datatypeElement, parserOptions); } - else if (datatypeObject is IPrimitive) + else if (datatypeObject is IPrimitive primitive) { - ParsePrimitive((IPrimitive)datatypeObject, datatypeElement); + ParsePrimitive(primitive, datatypeElement, parserOptions); } - else if (datatypeObject is IComposite) + else if (datatypeObject is IComposite composite) { - ParseComposite((IComposite)datatypeObject, datatypeElement); + ParseComposite(composite, datatypeElement, parserOptions); } } - ///

Returns a minimal amount of data from a message string, including only the + ///

Returns a minimal amount of data from a message string, including only the /// data needed to send a response to the remote system. This includes the /// following fields: /// @@ -399,7 +435,7 @@ public virtual void Parse(IType datatypeObject, XmlElement datatypeElement) /// (so the Message object is unavailable) but an error message must be sent /// back to the remote system including some of the information in the inbound /// message. This method parses only that required information, hopefully - /// avoiding the condition that caused the original error.

+ /// avoiding the condition that caused the original error.
///
public override ISegment GetCriticalResponseData(string message) { @@ -410,9 +446,9 @@ public override ISegment GetCriticalResponseData(string message) Terser.Set(criticalData, 2, 0, 1, 1, ParseLeaf(message, "MSH.2", 0)); Terser.Set(criticalData, 10, 0, 1, 1, ParseLeaf(message, "MSH.10", 0)); var procID = ParseLeaf(message, "MSH.11", 0); - if (procID == null || procID.Length == 0) + if (string.IsNullOrEmpty(procID)) { - procID = ParseLeaf(message, "PT.1", message.IndexOf("MSH.11")); + procID = ParseLeaf(message, "PT.1", message.IndexOf("MSH.11", StringComparison.Ordinal)); // this field is a composite in later versions } @@ -445,12 +481,13 @@ public override string GetAckID(string message) } } - public override string GetVersion(string message) + /// + public override string GetVersion(string message, ParserOptions parserOptions) { var version = ParseLeaf(message, "MSH.12", 0); if (version == null || version.Trim().Length == 0) { - version = ParseLeaf(message, "VID.1", message.IndexOf("MSH.12")); + version = ParseLeaf(message, "VID.1", message.IndexOf("MSH.12", StringComparison.Ordinal)); } return version; @@ -461,13 +498,39 @@ public override string GetVersion(string message) ///
/// The target node. /// - /// true if whitespaces should not be removed from node content; otherwise, false. + /// if whitespaces should not be removed from node content; otherwise, . /// protected internal virtual bool KeepAsOriginal(XmlNode node) { - return - node.Name != null - && concatKeepAsOriginalNodes.IndexOf(node.Name) != -1; + return KeepAsOriginal(node, DefaultParserOptions); + } + + /// + /// Checks if a node content should be kept as original (ie.: whitespaces won't be removed). + /// + /// The target node. + /// Contains configuration that will be applied when parsing. + /// + /// if whitespaces should not be removed from node content; otherwise, . + /// + protected internal virtual bool KeepAsOriginal(XmlNode node, ParserOptions parserOptions) + { + return parserOptions.DisableWhitespaceTrimmingOnAllXmlNodes + || parserOptions.XmlNodeNamesToDisableWhitespaceTrimming.Contains(node.LocalName) + || KeepAsOriginalNodes.Contains(node.LocalName, StringComparer.Ordinal); + } + + /// + /// Validates the namespace. + /// + /// Namespace to assert. + /// If provided namespace is not valid. + protected internal virtual void AssertNamespaceUri(string @namespace) + { + if (!NameSpace.Equals(@namespace, StringComparison.Ordinal)) + { + throw new HL7Exception($"Namespace URI must be {NameSpace}"); + } } /// @@ -475,16 +538,16 @@ protected internal virtual bool KeepAsOriginal(XmlNode node) /// This includes leading and trailing whitespace, and repeated space characters. Carriage returns, /// line feeds, and tabs are replaced with spaces. /// - protected internal virtual string RemoveWhitespace(string s) + protected internal virtual string RemoveWhitespace(string input) { - s = s.Replace('\r', ' '); - s = s.Replace('\n', ' '); - s = s.Replace('\t', ' '); + input = input.Replace('\r', ' ') + .Replace('\n', ' ') + .Replace('\t', ' '); var repeatedSpacesExist = true; while (repeatedSpacesExist) { - var loc = s.IndexOf(" "); + var loc = input.IndexOf(" ", StringComparison.Ordinal); if (loc < 0) { repeatedSpacesExist = false; @@ -492,14 +555,14 @@ protected internal virtual string RemoveWhitespace(string s) else { var buf = new StringBuilder(); - buf.Append(s.Substring(0, loc - 0)); + buf.Append(input.Substring(0, loc - 0)); buf.Append(" "); - buf.Append(s.Substring(loc + 2)); - s = buf.ToString(); + buf.Append(input.Substring(loc + 2)); + input = buf.ToString(); } } - return s.Trim(); + return input.Trim(); } /// @@ -510,13 +573,13 @@ protected internal virtual string RemoveWhitespace(string s) /// protected internal override IMessage DoParse(string message, string version, ParserOptions parserOptions) { - IMessage m = null; + IMessage m; // parse message string into a DOM document try { var doc = new XmlDocument(); - doc.Load(new StringReader(message)); + doc.LoadXml(message); m = ParseDocument(doc, version, parserOptions); } @@ -537,59 +600,84 @@ protected internal override IMessage DoParse(string message, string version, Par ///
/// Thrown if the data fields in the message do not permit encoding (e.g. required fields are null). /// Thrown if the requested encoding is not supported by this parser. - protected internal override string DoEncode(IMessage source, string encoding) + protected internal override string DoEncode(IMessage source, string encoding, ParserOptions parserOptions) { if (!SupportsEncoding("XML")) { throw new EncodingNotSupportedException("XMLParser supports only XML encoding"); } - return Encode(source); + return Encode(source, parserOptions); } /// /// Formats a Message object into an HL7 message string using this parser's /// default encoding (XML encoding). This method calls the abstract method. - /// encodeDocument(...) in order to obtain XML Document object + /// in order to obtain object /// representation of the Message, then serializes it to a String. /// /// Thrown if the data fields in the message do not permit encoding (e.g. required fields are null). - protected internal override string DoEncode(IMessage source) + protected internal override string DoEncode(IMessage source, ParserOptions parserOptions) { - if (source is GenericMessage) + Log.Info("XML-Encoding a GenericMessage is not covered by the specification."); + + var doc = EncodeDocument(source, parserOptions); + + var stringBuilder = new StringBuilder(); + var utf8StringWriter = new StringWriterWithEncoding(stringBuilder, Encoding.UTF8); + var xmlWriterSettings = new XmlWriterSettings { Indent = parserOptions.PrettyPrintEncodedXml, CloseOutput = true }; + + using (var writer = XmlWriter.Create(utf8StringWriter, xmlWriterSettings)) { - throw new HL7Exception( - "Can't XML-encode a GenericMessage. Message must have a recognized structure."); + doc.WriteTo(writer); } - var doc = EncodeDocument(source); - doc.DocumentElement.SetAttribute("xmlns", "urn:hl7-org:v2xml"); - - return doc.OuterXml; + return stringBuilder.ToString(); } /// - /// Attempts to retrieve the value of a leaf tag without using DOM or SAX. - /// This method searches the given message string for the given tag name, and returns - /// everything after the given tag and before the start of the next tag. Whitespace - /// is stripped. This is intended only for lead nodes, as the value is considered to + /// Attempts to retrieve the value of a leaf tag without using DOM or SAX. + /// This method searches the given message string for the given tag name, and returns + /// everything after the given tag and before the start of the next tag. + /// Whitespace is stripped. + /// + /// + /// This is intended only for lead nodes, as the value is considered to /// end at the start of the next tag, regardless of whether it is the matching end /// tag or some other nested tag. - ///
+ /// /// a string message in XML form. /// the name of the XML tag, e.g. "MSA.2". /// the character location at which to start searching. /// Thrown if the tag can not be found. protected internal virtual string ParseLeaf(string message, string tagName, int startAt) { - var tagStart = message.IndexOf("<" + tagName, startAt); + var prefix = string.Empty; + var matches = NameSpaceRegex.Match(message); + if (matches.Success) + { + var nameSpace = matches.Groups[1].Value; + if (!string.IsNullOrEmpty(nameSpace)) + { + prefix = nameSpace.Substring(1) + ":"; + } + } + + var tagStart = message.IndexOf($"<{prefix}{tagName}", startAt, StringComparison.Ordinal); if (tagStart < 0) { - tagStart = message.IndexOf("<" + tagName.ToUpper(), startAt); + tagStart = message.IndexOf($"<{prefix}{tagName.ToUpperInvariant()}", startAt, StringComparison.Ordinal); } - var valStart = message.IndexOf(">", tagStart) + 1; - var valEnd = message.IndexOf("<", valStart); + if (tagStart < 0) + { + throw new HL7Exception( + $"Couldn't find {tagName} in message beginning: {message.Substring(0, Math.Min(150, message.Length) - 0)}", + ErrorCode.REQUIRED_FIELD_MISSING); + } + + var valStart = message.IndexOf(">", tagStart, StringComparison.Ordinal) + 1; + var valEnd = message.IndexOf("<", valStart, StringComparison.Ordinal); string value; if (tagStart >= 0 && valEnd >= valStart) @@ -603,47 +691,20 @@ protected internal virtual string ParseLeaf(string message, string tagName, int ErrorCode.REQUIRED_FIELD_MISSING); } - return value; - } + // Escape codes, as defined at http://hdf.ncsa.uiuc.edu/HDF5/XML/xml_escape_chars.htm + value = Regex.Replace(value, """, "\""); + value = Regex.Replace(value, "'", "'"); + value = Regex.Replace(value, "&", "&"); + value = Regex.Replace(value, "<", "<"); + value = Regex.Replace(value, ">", ">"); - /// Populates a Composite type by looping through it's children, finding corresponding - /// Elements among the children of the given Element, and calling parse(Type, Element) for - /// each. - /// - private void ParseComposite(IComposite datatypeObject, XmlElement datatypeElement) - { - if (datatypeObject is GenericComposite) - { - // elements won't be named GenericComposite.x - var children = datatypeElement.ChildNodes; - var compNum = 0; - for (var i = 0; i < children.Count; i++) - { - if (Convert.ToInt16(children.Item(i).NodeType) == (short)XmlNodeType.Element) - { - Parse(datatypeObject[compNum], (XmlElement)children.Item(i)); - compNum++; - } - } - } - else - { - var children = datatypeObject.Components; - for (var i = 0; i < children.Length; i++) - { - var matchingElements = datatypeElement.GetElementsByTagName(MakeElementName(datatypeObject, i + 1)); - if (matchingElements.Count > 0) - { - Parse(children[i], (XmlElement)matchingElements.Item(0)); // components don't repeat - use 1st - } - } - } + return value; } /// /// Returns the expected XML element name for the given child of the given Segment. /// - private string MakeElementName(ISegment s, int child) + private static string MakeElementName(ISegment s, int child) { return $"{s.GetStructureName()}.{child}"; } @@ -651,26 +712,28 @@ private string MakeElementName(ISegment s, int child) /// /// Returns the expected XML element name for the given child of the given Composite. /// - private string MakeElementName(IComposite composite, int child) + private static string MakeElementName(IComposite composite, int child) { return $"{composite.TypeName}.{child}"; } /// - /// Populates the given Element with data from the given Type, by inserting - /// Elements corresponding to the Type's components and values. Returns true if - /// the given type contains a value (i.e. for Primitives, if getValue() doesn't - /// return null, and for Composites, if at least one underlying Primitive doesn't - /// return null). + /// Populates the given with data from the given , by inserting + /// XmlElements corresponding to the Type's components and values. /// - private bool Encode(IType datatypeObject, XmlElement datatypeElement) + /// + /// if the given type contains a value (i.e. for Primitives, if + /// doesn't return null, and for Composites, if at least one underlying + /// Primitive doesn't return null). + /// + private bool Encode(IType datatypeObject, XmlElement datatypeElement, ParserOptions parserOptions) { var hasData = false; // TODO: consider using a switch statement if (datatypeObject is Varies) { - hasData = EncodeVaries((Varies)datatypeObject, datatypeElement); + hasData = EncodeVaries((Varies)datatypeObject, datatypeElement, parserOptions); } else if (datatypeObject is IPrimitive) { @@ -678,67 +741,125 @@ private bool Encode(IType datatypeObject, XmlElement datatypeElement) } else if (datatypeObject is IComposite) { - hasData = EncodeComposite((IComposite)datatypeObject, datatypeElement); + hasData = EncodeComposite((IComposite)datatypeObject, datatypeElement, parserOptions); } return hasData; } - /// Encodes a Varies type by extracting it's data field and encoding that. Returns true - /// if the data field (or one of its components) contains a value. + /// + /// Encodes a Varies type by extracting it's data field and encoding that. /// - private bool EncodeVaries(Varies datatypeObject, XmlElement datatypeElement) + /// if the data field (or one of its components) contains a value. + private bool EncodeVaries(Varies datatypeObject, XmlElement datatypeElement, ParserOptions parserOptions) { var hasData = false; - if (datatypeObject.Data != null) + if (datatypeObject.Data is not null) { - hasData = Encode(datatypeObject.Data, datatypeElement); + hasData = Encode(datatypeObject.Data, datatypeElement, parserOptions); } return hasData; } - /// Encodes a Primitive in XML by adding it's value as a child of the given Element. - /// Returns true if the given Primitive contains a value. + /// + /// Encodes a Primitive in XML by adding it's value as a child of the given Element. /// + /// if the given Primitive contains a value. private bool EncodePrimitive(IPrimitive datatypeObject, XmlElement datatypeElement) { - var hasValue = false; - if (datatypeObject.Value != null && !datatypeObject.Value.Equals(string.Empty)) - { - hasValue = true; - } + var value = datatypeObject.Value; + var hasValue = !string.IsNullOrEmpty(value); - var t = datatypeElement.OwnerDocument.CreateTextNode(datatypeObject.Value); if (hasValue) { try { - datatypeElement.AppendChild(t); + var encoding = EncodingCharacters.FromMessage(datatypeObject.Message); + int pos; + var oldPos = 0; + var escaping = false; + + // Find next escape character + while ((pos = value.IndexOf(encoding.EscapeCharacter, oldPos)) >= 0) + { + // string until next escape character + var v = value.Substring(oldPos, pos - oldPos); + if (!escaping) + { + // currently in "text mode", so create textnode from it + if (v.Length > 0) + { + datatypeElement.AppendChild(datatypeElement.OwnerDocument.CreateTextNode(v)); + } + + escaping = true; + } + else + { + if (v.StartsWith(".", StringComparison.Ordinal) + || "H".Equals(v, StringComparison.Ordinal) + || "N".Equals(v, StringComparison.Ordinal)) + { + // currently in "escape mode", so create escape element from it + var escape = datatypeElement.OwnerDocument.CreateElement(EscapeNodeName, NameSpace); + escape.SetAttribute(EscapeAttrName, v); + datatypeElement.AppendChild(escape); + escaping = false; + } + else + { + // no proper escape sequence, assume text + datatypeElement.AppendChild( + datatypeElement.OwnerDocument + .CreateTextNode(encoding.EscapeCharacter + v)); + } + } + + oldPos = pos + 1; + } + + // create text from the remainder + if (oldPos <= value.Length) + { + var stringBuilder = new StringBuilder(); + + // If we are in escaping mode, there appears no closing escape character, + // so we treat the string as text + if (escaping) + { + stringBuilder.Append(encoding.EscapeCharacter); + } + + stringBuilder.Append(value.Substring(oldPos)); + datatypeElement.AppendChild( + datatypeElement.OwnerDocument.CreateTextNode(stringBuilder.ToString())); + } } - catch (Exception e) + catch (Exception ex) { - throw new DataTypeException("DOMException encoding Primitive: ", e); + throw new DataTypeException("Exception encoding Primitive: ", ex); } } return hasValue; } - /// Encodes a Composite in XML by looping through it's components, creating new + /// + /// Encodes a Composite in XML by looping through it's components, creating new /// children for each of them (with the appropriate names) and populating them by - /// calling encode(Type, Element) using these children. Returns true if at least - /// one component contains a value. + /// calling using these children. /// - private bool EncodeComposite(IComposite datatypeObject, XmlElement datatypeElement) + /// if at least one component contains a value. + private bool EncodeComposite(IComposite datatypeObject, XmlElement datatypeElement, ParserOptions parserOptions) { var components = datatypeObject.Components; var hasValue = false; for (var i = 0; i < components.Length; i++) { var name = MakeElementName(datatypeObject, i + 1); - var newNode = datatypeElement.OwnerDocument.CreateElement(name); - var componentHasValue = Encode(components[i], newNode); + var newNode = datatypeElement.OwnerDocument.CreateElement(name, NameSpace); + var componentHasValue = Encode(components[i], newNode, parserOptions); if (componentHasValue) { try @@ -757,21 +878,51 @@ private bool EncodeComposite(IComposite datatypeObject, XmlElement datatypeEleme return hasValue; } - private void ParseReps(ISegment segmentObject, XmlElement segmentElement, string fieldName, int fieldNum) + private void ParseReps(ISegment segmentObject, XmlElement segmentElement, string fieldName, int fieldNum, ParserOptions parserOptions) { - var reps = segmentElement.GetElementsByTagName(fieldName); + var reps = segmentElement.GetElementsByTagName(fieldName, NameSpace); for (var i = 0; i < reps.Count; i++) { - Parse(segmentObject.GetField(fieldNum, i), (XmlElement)reps.Item(i)); + Parse(segmentObject.GetField(fieldNum, i), (XmlElement)reps[i], parserOptions); } } /// - /// Parses an XML element into a Varies by determining whether the element is primitive or - /// composite, calling setData() on the Varies with a new generic primitive or composite as appropriate, - /// and then calling parse again with the new Type object. + /// Returns if the provided has any children which are + /// are also of type . /// - private void ParseVaries(Varies datatypeObject, XmlElement datatypeElement) + /// Element to test. + /// + private bool HasChildElement(XmlElement element) + { + var children = element.ChildNodes; + var hasElement = false; + var i = 0; + while (i < children.Count && !hasElement) + { + if (children[i].NodeType == XmlNodeType.Element + && !EscapeNodeName.Equals(children[i].Name, StringComparison.Ordinal)) + { + hasElement = true; + } + + i++; + } + + return hasElement; + } + + /// + /// Parses an into a by determining whether the element is + /// or , assigning the Varies.Data + /// with a new or as appropriate, and then + /// calling parse again with this newly assigned Varies.Data. + /// + /// The to parse into. + /// The to be parsed. + /// Contains configuration that will be applied when parsing. + /// if the data did not match the expected type rules. + private void ParseVaries(Varies datatypeObject, XmlElement datatypeElement, ParserOptions parserOptions) { // figure out what data type it holds if (!HasChildElement(datatypeElement)) @@ -785,71 +936,167 @@ private void ParseVaries(Varies datatypeObject, XmlElement datatypeElement) datatypeObject.Data = new GenericComposite(datatypeObject.Message); } - Parse(datatypeObject.Data, datatypeElement); + Parse(datatypeObject.Data, datatypeElement, parserOptions); } - /// Returns true if any of the given element's children are elements. - private bool HasChildElement(XmlElement e) + /// + /// Populates the given object with data from the given . + /// + /// The to parse into. + /// The to be parsed. + /// Contains configuration that will be applied when parsing. + /// + /// If any of the XmlElement.ChildNodes of type + /// which should be escaped from + /// have an invalid namespace. + /// + private void ParsePrimitive(IPrimitive datatypeObject, XmlElement datatypeElement, ParserOptions parserOptions) { - var children = e.ChildNodes; - var hasElement = false; - var c = 0; - while (c < children.Count && !hasElement) + var children = datatypeElement.ChildNodes; + var builder = new StringBuilder(); + + for (var childIndex = 0; childIndex < children.Count; childIndex++) { - if (Convert.ToInt16(children.Item(c).NodeType) == (short)XmlNodeType.Element) + var child = children[childIndex]; + try { - hasElement = true; - } + if (child.NodeType == XmlNodeType.Text) + { + var value = child.Value; + if (!string.IsNullOrEmpty(value)) + { + var valueToAppend = KeepAsOriginal(child.ParentNode, parserOptions) + ? value + : RemoveWhitespace(value); - c++; + builder.Append(valueToAppend); + } + } + else if (child.NodeType == XmlNodeType.Element && EscapeNodeName.Equals(child.LocalName)) + { + AssertNamespaceUri(child.NamespaceURI); + + var encoding = EncodingCharacters.FromMessage(datatypeObject.Message); + var element = (XmlElement)child; + var attribute = element.GetAttribute(EscapeAttrName).Trim(); + + if (!string.IsNullOrEmpty(attribute)) + { + builder.Append(encoding.EscapeCharacter) + .Append(attribute) + .Append(encoding.EscapeCharacter); + } + } + } + catch (Exception ex) + { + Log.Error("Error parsing primitive value from TEXT_NODE", ex); + } } - return hasElement; + datatypeObject.Value = builder.ToString(); } - /// Parses a primitive type by filling it with text child, if any. - private void ParsePrimitive(IPrimitive datatypeObject, XmlElement datatypeElement) + /// + /// Populates the provided by looping through it's , finding corresponding + /// XmlElements among the children of the given , and + /// calling for each. + /// + /// The to parse into. + /// The to be parsed. + /// Contains configuration that will be applied when parsing. + /// + /// If any of the XmlElement.ChildNodes of type + /// which should be escaped from + /// have an invalid namespace. + /// + private void ParseComposite(IComposite datatypeObject, XmlElement datatypeElement, ParserOptions parserOptions) { - var children = datatypeElement.ChildNodes; - var c = 0; - var full = false; - while (c < children.Count && !full) + if (datatypeObject is GenericComposite) { - var child = children.Item(c++); - if (Convert.ToInt16(child.NodeType) == (short)XmlNodeType.Text) + // elements won't be named GenericComposite.x + var children = datatypeElement.ChildNodes; + var componentIndex = 0; + for (var i = 0; i < children.Count; i++) { - try + if (children[i].NodeType != XmlNodeType.Element) { - if (child.Value != null && !child.Value.Equals(string.Empty)) - { - if (KeepAsOriginal(child.ParentNode)) - { - datatypeObject.Value = child.Value; - } - else - { - datatypeObject.Value = RemoveWhitespace(child.Value); - } - } + continue; } - catch (Exception e) + + var nextElement = (XmlElement)children[i]; + AssertNamespaceUri(nextElement.NamespaceURI); + + var localName = nextElement.LocalName; + var dotPosition = localName.IndexOf(".", StringComparison.Ordinal); + if (dotPosition > -1) + { + componentIndex = int.Parse(localName.Substring(dotPosition + 1)) - 1; + } + else + { + Log.Debug( + $"Datatype element {datatypeElement.LocalName} doesn't have a valid numbered name, using default index of {componentIndex}"); + } + + var nextComponent = datatypeObject[componentIndex]; + + Parse(nextComponent, nextElement, parserOptions); + componentIndex++; + } + } + else + { + var children = datatypeObject.Components; + for (var i = 0; i < children.Length; i++) + { + var matchingElements = datatypeElement.GetElementsByTagName(MakeElementName(datatypeObject, i + 1), NameSpace); + if (matchingElements.Count > 0) + { + // components don't repeat - use 1st + Parse(children[i], (XmlElement)matchingElements[0], parserOptions); + } + } + + var nextExtraComponent = 0; + bool foundExtraComponent; + do + { + foundExtraComponent = false; + var matchingElements = + datatypeElement.GetElementsByTagName( + MakeElementName(datatypeObject, children.Length + nextExtraComponent + 1), NameSpace); + + if (matchingElements.Count > 0) { - Log.Error("Error parsing primitive value from TEXT_NODE", e); + var extraComponent = datatypeObject.ExtraComponents.GetComponent(nextExtraComponent); + Parse(extraComponent, (XmlElement)matchingElements[0], parserOptions); + foundExtraComponent = true; } - full = true; + nextExtraComponent++; } + while (foundExtraComponent); } } private class AnonymousClassXMLParser : XMLParser { + public AnonymousClassXMLParser() + { + } + + public AnonymousClassXMLParser(IModelClassFactory factory) + : base(factory) + { + } + public override IMessage ParseDocument(XmlDocument xmlMessage, string version, ParserOptions parserOptions) { return null; } - public override XmlDocument EncodeDocument(IMessage source) + public override XmlDocument EncodeDocument(IMessage source, ParserOptions parserOptions) { return null; } @@ -862,6 +1109,22 @@ public override string GetVersion(string message) { return null; } + + protected internal override string DoEncode(IMessage source, ParserOptions parserOptions) + { + return null; + } + } + + private sealed class StringWriterWithEncoding : StringWriter + { + public StringWriterWithEncoding(StringBuilder builder, Encoding encoding) + : base(builder) + { + this.Encoding = encoding; + } + + public override Encoding Encoding { get; } } } } \ No newline at end of file diff --git a/src/NHapi.Base/PreParser/Er7.cs b/src/NHapi.Base/PreParser/Er7.cs index ace4f5347..41a865ef4 100644 --- a/src/NHapi.Base/PreParser/Er7.cs +++ b/src/NHapi.Base/PreParser/Er7.cs @@ -234,10 +234,6 @@ private static void ParseSegmentWhole( private class Er7SegmentHandler { - internal int SegmentRepetitionIndex; - internal string SegmentId; - internal IList MessageMasks; - private readonly EncodingCharacters encodingCharacters; private readonly IDictionary props; @@ -249,6 +245,12 @@ public Er7SegmentHandler(EncodingCharacters encodingCharacters, IDictionary 4; + internal int SegmentRepetitionIndex { get; set; } + + internal string SegmentId { get; set; } + + internal IList MessageMasks { get; set; } + public char Delimiter(int level) => level switch { 0 => this.encodingCharacters.FieldSeparator, diff --git a/src/NHapi.Base/SupportClass.cs b/src/NHapi.Base/SupportClass.cs index f545430b6..735e66612 100644 --- a/src/NHapi.Base/SupportClass.cs +++ b/src/NHapi.Base/SupportClass.cs @@ -1460,13 +1460,13 @@ private void DoParsing() for (int i = 0; i < reader.AttributeCount; i++) { reader.MoveToAttribute(i); - string prefixName = (reader.Name.IndexOf(":") > 0) - ? reader.Name.Substring(reader.Name.IndexOf(":") + 1, reader.Name.Length - reader.Name.IndexOf(":") - 1) + string prefixName = (reader.Name.IndexOf(":", StringComparison.Ordinal) > 0) + ? reader.Name.Substring(reader.Name.IndexOf(":", StringComparison.Ordinal) + 1, reader.Name.Length - reader.Name.IndexOf(":") - 1) : string.Empty; - string prefix = (reader.Name.IndexOf(":") > 0) - ? reader.Name.Substring(0, reader.Name.IndexOf(":")) + string prefix = (reader.Name.IndexOf(":", StringComparison.Ordinal) > 0) + ? reader.Name.Substring(0, reader.Name.IndexOf(":", StringComparison.Ordinal)) : reader.Name; - bool IsXmlns = prefix.ToLower().Equals("xmlns"); + bool IsXmlns = prefix.ToLower().Equals("xmlns", StringComparison.Ordinal); if (namespaceAllowed) { if (!IsXmlns) diff --git a/src/NHapi.Base/Util/Terser.cs b/src/NHapi.Base/Util/Terser.cs index 6175cab9b..6505df521 100644 --- a/src/NHapi.Base/Util/Terser.cs +++ b/src/NHapi.Base/Util/Terser.cs @@ -148,10 +148,10 @@ public static string Get(ISegment segment, int field, int rep, int component, in } /// Sets the string value of the Primitive at the given location. - public static void Set(ISegment segment, int field, int rep, int component, int subcomponent, string value_Renamed) + public static void Set(ISegment segment, int field, int rep, int component, int subcomponent, string value) { var prim = GetPrimitive(segment, field, rep, component, subcomponent); - prim.Value = value_Renamed; + prim.Value = value; } [Obsolete("This method has been replaced by 'GetPrimitive'.")] @@ -372,7 +372,7 @@ public virtual ISegment GetSegment(string segSpec) { ISegment segment = null; - if (segSpec.Substring(0, 1 - 0).Equals("/")) + if (segSpec.StartsWith("/", StringComparison.Ordinal)) { Finder.Reset(); } @@ -522,7 +522,7 @@ private static int NumStandardComponents(IType type) /// Gets path information from a path spec. private PathSpec ParsePathSpec(string spec) { - var ps = new PathSpec(this); + var ps = new PathSpec(); if (spec.StartsWith(".", StringComparison.Ordinal)) { @@ -564,13 +564,6 @@ private PathSpec ParsePathSpec(string spec) /// Struct for information about a step in a segment path. private class PathSpec { - public PathSpec(Terser enclosingInstance) - { - EnclosingInstance = enclosingInstance; - } - - public Terser EnclosingInstance { get; } - public string Pattern { get; set; } public bool IsGroup { get; set; } diff --git a/src/NHapi.SourceGeneration/Generators/DataTypeGenerator.cs b/src/NHapi.SourceGeneration/Generators/DataTypeGenerator.cs index ead21d955..a438d62ed 100644 --- a/src/NHapi.SourceGeneration/Generators/DataTypeGenerator.cs +++ b/src/NHapi.SourceGeneration/Generators/DataTypeGenerator.cs @@ -333,7 +333,7 @@ private static string MakePrimitive(string datatype, string description, string source.Append("\t///\r\n"); source.Append("\tpublic string getVersion() {\r\n"); source.Append("\t return \""); - if (version.IndexOf("UCH") > -1) + if (version.IndexOf("UCH", StringComparison.Ordinal) > -1) { source.Append("2.3"); } diff --git a/src/NHapi.SourceGeneration/NHapi.SourceGeneration.csproj b/src/NHapi.SourceGeneration/NHapi.SourceGeneration.csproj index cc566b0c9..bb46a4174 100644 --- a/src/NHapi.SourceGeneration/NHapi.SourceGeneration.csproj +++ b/src/NHapi.SourceGeneration/NHapi.SourceGeneration.csproj @@ -15,8 +15,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/NHapi.NUnit.SourceGeneration/NHapi.NUnit.SourceGeneration.csproj b/tests/NHapi.NUnit.SourceGeneration/NHapi.NUnit.SourceGeneration.csproj index 5977b12df..86c570677 100644 --- a/tests/NHapi.NUnit.SourceGeneration/NHapi.NUnit.SourceGeneration.csproj +++ b/tests/NHapi.NUnit.SourceGeneration/NHapi.NUnit.SourceGeneration.csproj @@ -26,7 +26,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/NHapi.NUnit/CustomZSegmentTest.cs b/tests/NHapi.NUnit/CustomZSegmentTest.cs index 9ce824a27..fbf8b9046 100644 --- a/tests/NHapi.NUnit/CustomZSegmentTest.cs +++ b/tests/NHapi.NUnit/CustomZSegmentTest.cs @@ -4,7 +4,6 @@ namespace NHapi.NUnit using global::NUnit.Framework; - using NHapi.Base.Model; using NHapi.Base.Parser; using NHapi.Model.V22_ZSegments; using NHapi.Model.V22_ZSegments.Message; @@ -52,9 +51,9 @@ public void ParseADT_A08() Assert.IsNotNull(m); - Console.WriteLine("Type: " + m.GetType()); + Console.WriteLine($"Type: {m.GetType()}"); - var adtA08 = m as ADT_A08; + var adtA08 = (ADT_A08)m; // verify some Z segment data Assert.AreEqual("45789", adtA08.ZIN.AccidentData.Id.Value); diff --git a/tests/NHapi.NUnit/NHapi.NUnit.csproj b/tests/NHapi.NUnit/NHapi.NUnit.csproj index 843c2156f..cfc6e26ea 100644 --- a/tests/NHapi.NUnit/NHapi.NUnit.csproj +++ b/tests/NHapi.NUnit/NHapi.NUnit.csproj @@ -5,27 +5,24 @@ false - - - - - + + ..\..\NHapi.snk + true + - - PreserveNewest - - - PreserveNewest - - + + PreserveNewest - - + + + + + PreserveNewest @@ -63,18 +60,13 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - PreserveNewest - - - @@ -84,4 +76,4 @@ - + \ No newline at end of file diff --git a/tests/NHapi.NUnit/Parser/LegacyPipeParserV231Tests.cs b/tests/NHapi.NUnit/Parser/LegacyPipeParserV231Tests.cs index b66c3367b..32de83407 100644 --- a/tests/NHapi.NUnit/Parser/LegacyPipeParserV231Tests.cs +++ b/tests/NHapi.NUnit/Parser/LegacyPipeParserV231Tests.cs @@ -73,7 +73,7 @@ public void ParseORMo01ToXml() var ormo01 = m as ORM_O01; - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(ormo01); @@ -100,7 +100,7 @@ public void ParseORRo02ToXml() var msg = m as ORR_O02; - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(msg); @@ -145,7 +145,7 @@ public void ParseORUr01LongToXml() var msg = m as ORU_R01; - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(msg); @@ -199,7 +199,7 @@ public void ParseORFR04ToXML() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); @@ -231,7 +231,7 @@ public void ParseORMwithOBXToXML() Assert.IsNotNull(msgObj); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(msgObj); @@ -264,7 +264,7 @@ public void ParseORMwithCompleteOBXToXML() Assert.IsNotNull(msgObj); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(msgObj); @@ -277,7 +277,7 @@ public void ParseXMLToHL7() { var message = GetQRYR02XML(); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var m = xmlParser.Parse(message); var qryR02 = m as QRY_R02; @@ -312,12 +312,12 @@ public void ParseORFR04ToXmlNoOCR() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); Assert.IsNotNull(recoveredMessage); - Assert.IsFalse(recoveredMessage.IndexOf("ORC") > -1, "Returned Message added ORC segment."); + Assert.IsFalse(recoveredMessage.IndexOf("ORC", StringComparison.Ordinal) > -1, "Returned Message added ORC segment."); } [Test] @@ -340,12 +340,12 @@ public void ParseORFR04ToXmlNoNTE() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); Assert.IsNotNull(recoveredMessage); - Assert.IsFalse(recoveredMessage.IndexOf("NTE") > -1, "Returned Message added ORC segment."); + Assert.IsFalse(recoveredMessage.IndexOf("NTE", StringComparison.Ordinal) > -1, "Returned Message added ORC segment."); } [Test] @@ -436,12 +436,12 @@ public void ParseORFR04FromDHTest() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); Assert.IsNotNull(recoveredMessage); - Assert.IsFalse(recoveredMessage.IndexOf("NTE") > -1, "Returned Message added ORC segment."); + Assert.IsFalse(recoveredMessage.IndexOf("NTE", StringComparison.Ordinal) > -1, "Returned Message added ORC segment."); } public void TestDHPatient1111111() @@ -455,12 +455,12 @@ public void TestDHPatient1111111() Assert.IsNotNull(orfR04); object range = orfR04.GetQUERY_RESPONSE().GetORDER().GetOBSERVATION().OBX.GetObservationValue(1); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); Assert.IsNotNull(recoveredMessage); - Assert.IsFalse(recoveredMessage.IndexOf("NTE") > -1, "Returned Message added ORC segment."); + Assert.IsFalse(recoveredMessage.IndexOf("NTE", StringComparison.Ordinal) > -1, "Returned Message added ORC segment."); } private static string GetQRYR02XML() @@ -566,9 +566,12 @@ public void TestOBXDataTypes() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); + + Assert.IsNotNull(recoveredMessage); + Assert.IsFalse(string.Empty.Equals(recoveredMessage)); } private static string GetDHPatient1111111() diff --git a/tests/NHapi.NUnit/Parser/LegacyPipeParserV23Tests.cs b/tests/NHapi.NUnit/Parser/LegacyPipeParserV23Tests.cs index 5e1d27b05..64a157c54 100644 --- a/tests/NHapi.NUnit/Parser/LegacyPipeParserV23Tests.cs +++ b/tests/NHapi.NUnit/Parser/LegacyPipeParserV23Tests.cs @@ -106,7 +106,7 @@ public void ParseORFR04ToXML() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); @@ -119,7 +119,7 @@ public void ParseXMLToHL7() { var message = GetQRYR02XML(); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var m = xmlParser.Parse(message); var qryR02 = m as QRY_R02; @@ -154,12 +154,12 @@ public void ParseORFR04ToXmlNoOCR() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); Assert.IsNotNull(recoveredMessage); - Assert.IsFalse(recoveredMessage.IndexOf("ORC") > -1, "Returned message added ORC segment."); + Assert.IsFalse(recoveredMessage.IndexOf("ORC", StringComparison.Ordinal) > -1, "Returned message added ORC segment."); } [Test] @@ -194,9 +194,12 @@ public void TestOBXDataTypes() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); + + Assert.IsNotNull(recoveredMessage); + Assert.IsFalse(string.Empty.Equals(recoveredMessage)); } [Test] @@ -219,12 +222,12 @@ public void ParseORFR04ToXmlNoNTE() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); Assert.IsNotNull(recoveredMessage); - Assert.IsFalse(recoveredMessage.IndexOf("NTE") > -1, "Returned message added ORC segment."); + Assert.IsFalse(recoveredMessage.IndexOf("NTE", StringComparison.Ordinal) > -1, "Returned message added ORC segment."); } private static string GetQRYR02XML() diff --git a/tests/NHapi.NUnit/Parser/LegacyPipeParserV24Tests.cs b/tests/NHapi.NUnit/Parser/LegacyPipeParserV24Tests.cs index cfe9e8bed..55eec100e 100644 --- a/tests/NHapi.NUnit/Parser/LegacyPipeParserV24Tests.cs +++ b/tests/NHapi.NUnit/Parser/LegacyPipeParserV24Tests.cs @@ -73,7 +73,7 @@ public void ParseORFR04ToXML() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); @@ -86,7 +86,7 @@ public void ParseXMLToHL7() { var message = GetQRYR02XML(); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var m = xmlParser.Parse(message); var qryR02 = m as QRY_R02; @@ -121,7 +121,7 @@ public void ParseORFR04ToXmlNoOCR() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); @@ -161,9 +161,12 @@ public void TestOBXDataTypes() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); + + Assert.IsNotNull(recoveredMessage); + Assert.IsFalse(string.Empty.Equals(recoveredMessage)); } [Test] @@ -186,7 +189,7 @@ public void ParseORFR04ToXmlNoNTE() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new LegacyDefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); diff --git a/tests/NHapi.NUnit/Parser/ParserBaseTests.cs b/tests/NHapi.NUnit/Parser/ParserBaseTests.cs index 7b5fc641c..ef0527bd8 100644 --- a/tests/NHapi.NUnit/Parser/ParserBaseTests.cs +++ b/tests/NHapi.NUnit/Parser/ParserBaseTests.cs @@ -176,16 +176,35 @@ public void TestParserDoesntFailWithoutValidationExceptionHandlerFactory() { var adt = new ADT_A01(); + SetMessageHeader(adt, "ADT", "A01", "T"); + var sut = new PipeParser(); var xmlParser = new DefaultXMLParser(); var msg = sut.Encode(adt); - sut.Parse(msg); + adt = (ADT_A01)sut.Parse(msg); msg = xmlParser.Encode(adt); xmlParser.Parse(msg); } #endregion + + private static void SetMessageHeader(IMessage msg, string messageCode, string messageTriggerEvent, string processingId) + { + var msh = (ISegment)msg.GetStructure("MSH"); + + var version27 = new Version("2.7"); + var messageVersion = new Version(msg.Version); + + Terser.Set(msh, 1, 0, 1, 1, "|"); + Terser.Set(msh, 2, 0, 1, 1, version27 > messageVersion ? "^~\\&" : "^~\\&#"); + Terser.Set(msh, 7, 0, 1, 1, DateTime.Now.ToString("yyyyMMddHHmmssK")); + Terser.Set(msh, 9, 0, 1, 1, messageCode); + Terser.Set(msh, 9, 0, 2, 1, messageTriggerEvent); + Terser.Set(msh, 10, 0, 1, 1, Guid.NewGuid().ToString()); + Terser.Set(msh, 11, 0, 1, 1, processingId); + Terser.Set(msh, 12, 0, 1, 1, msg.Version); + } } -} +} \ No newline at end of file diff --git a/tests/NHapi.NUnit/Parser/PipeParserTests.cs b/tests/NHapi.NUnit/Parser/PipeParserTests.cs index fa25d287a..f855c7bad 100644 --- a/tests/NHapi.NUnit/Parser/PipeParserTests.cs +++ b/tests/NHapi.NUnit/Parser/PipeParserTests.cs @@ -175,24 +175,6 @@ public void UnEscapesData() Assert.AreEqual(expectedResult, segmentData); } - /// - /// Check that an is thrown when a null is - /// provided to Parse method calls. - /// - [Test] - public void ParseWithNullConfigThrows() - { - var parser = new PipeParser(); - IMessage nullMessage = null; - const string version = "2.5.1"; - ParserOptions nullConfiguration = null; - - Assert.Throws(() => parser.Parse(GetMessage(), nullConfiguration)); - Assert.Throws(() => - parser.Parse(nullMessage, GetMessage(), nullConfiguration)); - Assert.Throws(() => parser.Parse(GetMessage(), version, nullConfiguration)); - } - private static void SetMessageHeader(Model.V251.Message.OML_O21 msg, string messageCode, string messageTriggerEvent, string processingId) { var msh = msg.MSH; diff --git a/tests/NHapi.NUnit/Parser/PipeParserV231Tests.cs b/tests/NHapi.NUnit/Parser/PipeParserV231Tests.cs index bed4124d0..7eaf76f1e 100644 --- a/tests/NHapi.NUnit/Parser/PipeParserV231Tests.cs +++ b/tests/NHapi.NUnit/Parser/PipeParserV231Tests.cs @@ -199,7 +199,7 @@ public void ParseORFR04ToXML() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new DefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); @@ -312,12 +312,12 @@ public void ParseORFR04ToXmlNoOCR() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new DefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); Assert.IsNotNull(recoveredMessage); - Assert.IsFalse(recoveredMessage.IndexOf("ORC") > -1, "Returned Message added ORC segment."); + Assert.IsFalse(recoveredMessage.IndexOf("ORC", StringComparison.Ordinal) > -1, "Returned Message added ORC segment."); } [Test] @@ -340,12 +340,12 @@ public void ParseORFR04ToXmlNoNTE() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new DefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); Assert.IsNotNull(recoveredMessage); - Assert.IsFalse(recoveredMessage.IndexOf("NTE") > -1, "Returned Message added ORC segment."); + Assert.IsFalse(recoveredMessage.IndexOf("NTE", StringComparison.Ordinal) > -1, "Returned Message added ORC segment."); } [Test] @@ -436,12 +436,12 @@ public void ParseORFR04FromDHTest() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new DefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); Assert.IsNotNull(recoveredMessage); - Assert.IsFalse(recoveredMessage.IndexOf("NTE") > -1, "Returned Message added ORC segment."); + Assert.IsFalse(recoveredMessage.IndexOf("NTE", StringComparison.Ordinal) > -1, "Returned Message added ORC segment."); } public void TestDHPatient1111111() @@ -455,12 +455,12 @@ public void TestDHPatient1111111() Assert.IsNotNull(orfR04); object range = orfR04.GetQUERY_RESPONSE().GetORDER().GetOBSERVATION().OBX.GetObservationValue(1); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new DefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); Assert.IsNotNull(recoveredMessage); - Assert.IsFalse(recoveredMessage.IndexOf("NTE") > -1, "Returned Message added ORC segment."); + Assert.IsFalse(recoveredMessage.IndexOf("NTE", StringComparison.Ordinal) > -1, "Returned Message added ORC segment."); } private static string GetQRYR02XML() @@ -566,9 +566,12 @@ public void TestOBXDataTypes() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new DefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); + + Assert.IsNotNull(recoveredMessage); + Assert.IsFalse(string.Empty.Equals(recoveredMessage)); } private static string GetDHPatient1111111() diff --git a/tests/NHapi.NUnit/Parser/PipeParserV23Tests.cs b/tests/NHapi.NUnit/Parser/PipeParserV23Tests.cs index c2df94112..efaf2878e 100644 --- a/tests/NHapi.NUnit/Parser/PipeParserV23Tests.cs +++ b/tests/NHapi.NUnit/Parser/PipeParserV23Tests.cs @@ -106,7 +106,7 @@ public void ParseORFR04ToXML() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new DefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); @@ -154,7 +154,7 @@ public void ParseORFR04ToXmlNoOCR() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new DefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); @@ -194,9 +194,12 @@ public void TestOBXDataTypes() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new DefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); + + Assert.IsNotNull(recoveredMessage); + Assert.IsFalse(string.Empty.Equals(recoveredMessage)); } [Test] @@ -219,7 +222,7 @@ public void ParseORFR04ToXmlNoNTE() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new DefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); diff --git a/tests/NHapi.NUnit/Parser/PipeParserV24Tests.cs b/tests/NHapi.NUnit/Parser/PipeParserV24Tests.cs index 2d983f7dd..65d955339 100644 --- a/tests/NHapi.NUnit/Parser/PipeParserV24Tests.cs +++ b/tests/NHapi.NUnit/Parser/PipeParserV24Tests.cs @@ -73,7 +73,7 @@ public void ParseORFR04ToXML() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new DefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); @@ -121,7 +121,7 @@ public void ParseORFR04ToXmlNoOCR() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new DefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); @@ -161,9 +161,12 @@ public void TestOBXDataTypes() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new DefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); + + Assert.IsNotNull(recoveredMessage); + Assert.IsFalse(string.Empty.Equals(recoveredMessage)); } [Test] @@ -186,7 +189,7 @@ public void ParseORFR04ToXmlNoNTE() Assert.IsNotNull(orfR04); - XMLParser xmlParser = new DefaultXMLParser(); + var xmlParser = new DefaultXMLParser(); var recoveredMessage = xmlParser.Encode(orfR04); diff --git a/tests/NHapi.NUnit/Parser/XMLParserTests.cs b/tests/NHapi.NUnit/Parser/XMLParserTests.cs new file mode 100644 index 000000000..c8056098b --- /dev/null +++ b/tests/NHapi.NUnit/Parser/XMLParserTests.cs @@ -0,0 +1,394 @@ +namespace NHapi.NUnit.Parser +{ + using System; + using System.IO; + using System.Text.RegularExpressions; + + using global::NUnit.Framework; + + using NHapi.Base.Model; + using NHapi.Base.Parser; + using NHapi.Base.Util; + using NHapi.Model.V25.Datatype; + using NHapi.Model.V25.Message; + + using ADT_A01 = NHapi.Model.V23.Message.ADT_A01; + using ED = NHapi.Model.V25.Datatype.ED; + using ORU_R01 = NHapi.Model.V25.Message.ORU_R01; + using ST = NHapi.Model.V25.Datatype.ST; + + [TestFixture] + public class XMLParserTests + { + private static readonly string TestDataDir = $"{TestContext.CurrentContext.TestDirectory}/TestData/Parser"; + + [Test] + public void Encode_DuplicateSegmentInStructureDefinition_IsHandledCorrectly() + { + // Arrange + var message = File.ReadAllText($"{TestDataDir}/adt_a17.xml"); + var defaultXmlParser = new DefaultXMLParser(); + var pipeParser = new PipeParser(); + + // Act + var parsed = defaultXmlParser.Parse(message); + var er7Encoded = pipeParser.Encode(parsed); + var er7Message = Regex.Replace(er7Encoded, "\\r", "\\r\\n"); + + var firstIndex = er7Message.IndexOf("PID", StringComparison.Ordinal); + var secondIndex = er7Message.IndexOf("PID", firstIndex + 1, StringComparison.Ordinal); + var thirdIndex = er7Message.IndexOf("PID", secondIndex + 1, StringComparison.Ordinal); + + // Assert + Assert.True(firstIndex > 0); + Assert.True(secondIndex > firstIndex); + Assert.AreEqual(-1, thirdIndex, $"Found third PID {firstIndex} {secondIndex} {thirdIndex}:\r\n{er7Message}"); + } + + [Test] + public void Parse_XmlContainsExtraComponents_HandledCorrectly() + { + // Arrange + var xmlMessage = File.ReadAllText($"{TestDataDir}/extracmp_xml.xml"); + var xmlParser = new DefaultXMLParser(); + var pipeParser = new PipeParser(); + + var message = xmlParser.Parse(xmlMessage); + var er7Encoded = pipeParser.Encode(message); + + Assert.IsTrue(er7Encoded.Contains("HD.4")); + Assert.IsTrue(er7Encoded.Contains("HD.5")); + } + + [TestCase("12", "12")] + [TestCase(" help >>> *** 12 \r", "12")] + [TestCase("x", null)] + [TestCase("x", null)] + public void GetAckID_ValidInput_ReturnsExpectedResult(string input, string expected) + { + // Arrange + var parser = new DefaultXMLParser(); + + // Act / Assert + Assert.AreEqual(expected, parser.GetAckID(input)); + } + + [Test] + public void GetAckID_ValidInput_ReturnsExpectedResult() + { + // Arrange + var expected = "876"; + var xmlMessage = File.ReadAllText($"{TestDataDir}/get_ack_id.xml"); + var parser = new DefaultXMLParser(); + + // Act / Assert + Assert.AreEqual(expected, parser.GetAckID(xmlMessage)); + } + + [TestCase("\r|\r^~\\&\r", "XML")] + [TestCase("blorg gablorg", null)] + public void GetEncoding_ValidInput_ReturnsExpectedResult(string input, string expected) + { + // Arrange + var parser = new DefaultXMLParser(); + + // Act / Assert + Assert.AreEqual(expected, parser.GetEncoding(input)); + } + + [Test] + public void GetVersion_ValidInput_ReturnsExpectedResult() + { + // Arrange + var expected = "2.4"; + var xmlMessage = File.ReadAllText($"{TestDataDir}/get_version.xml"); + var parser = new DefaultXMLParser(); + + // Act / Assert + Assert.AreEqual(expected, parser.GetVersion(xmlMessage)); + } + + [TestCase("\t\r\nhello ", "hello")] + [TestCase(" hello \t \rthere\r\n", "hello there")] + public void RemoveWhitespace_ValidInput_ReturnsExpectedResult(string input, string expected) + { + // Arrange + var parser = new DefaultXMLParser(); + + // Act / Assert + Assert.AreEqual(expected, parser.RemoveWhitespace(input)); + } + + [Test] + public void Parse_XmlHasNamespaces_ReturnsExpectedResult() + { + // Arrange + var expectedVersion = "2.2"; + var expectedMsh7 = "19951010134000"; + var xmlMessage = File.ReadAllText($"{TestDataDir}/parse_and_encode_with_ns.xml"); + var parser = new DefaultXMLParser(); + var message = parser.Parse(xmlMessage); + var terser = new Terser(message); + + // Act / Assert + Assert.AreEqual(expectedVersion, parser.GetVersion(xmlMessage)); + Assert.AreEqual(expectedMsh7, terser.Get("MSH-7")); + } + + [Test] + public void GetCriticalResponseData_ValidInput_ReturnsExpectedResult() + { + // Arrange + var xmlMessage = File.ReadAllText($"{TestDataDir}/critical_response_data.xml"); + var parser = new DefaultXMLParser(); + var segment = parser.GetCriticalResponseData(xmlMessage); + var actual = segment.GetField(2, 0); + var expected = "^~\\&"; + + // Act / Assert + Assert.AreEqual(expected, actual.ToString()); + } + + [Test] + public void Parse_OruR01_CorrectlyHandlesEDEscaping() + { + // Arrange + var xmlMessage = File.ReadAllText($"{TestDataDir}/ed_issue.xml"); + var parser = new DefaultXMLParser(); + var message = parser.Parse(xmlMessage) as ORU_R01; + + // Act + var obx5 = + message?.GetPATIENT_RESULT().GetORDER_OBSERVATION().GetOBSERVATION(1) + .OBX.GetObservationValue(0).Data as ED; + + // Assert + Assert.True(obx5?.Data.Value.StartsWith("JVBERi0xLjMKJeLjz9MKCjEgMCBvYmoKPDwgL1R5cG")); + } + + [Test] + public void Parse_FollowedByEncode_WorksAsExpected() + { + // Arrange + var xmlMessage = File.ReadAllText($"{TestDataDir}/parse_and_encode.xml"); + var xmlParser = new DefaultXMLParser(); + + var oruR01 = new NHapi.Model.V26.Message.ORU_R01(); + + // Action + xmlParser.Parse(oruR01, xmlMessage); + var encodedOruR01 = xmlParser.Encode(oruR01); + + // Assert + Assert.AreEqual("LABMI1199510101340007", oruR01.MSH.MessageControlID.Value); + StringAssert.Contains("LABMI1199510101340007", encodedOruR01); + } + + [Test] + public void Parse_OmdO03_OrderDietSegmentIsNotMissing() + { + // Arrange + var xmlMessage = File.ReadAllText($"{TestDataDir}/OMD_O03.xml"); + var xmlParser = new DefaultXMLParser(); + + // Action + var omdO03 = (OMD_O03)xmlParser.Parse(xmlMessage); + + // Assert + Assert.AreEqual("S", omdO03.GetORDER_DIET().DIET.GetODS().Type.Value); + } + + [Test] + public void Parse_EncodedMessageIsModifiedWithEscapeSequence_IsParsedCorrectly() + { + // Arrange + var obx5Value = "CONTENT"; + + var oruR01 = new ORU_R01(); + + SetMessageHeader(oruR01, "ORU", "R01", "T"); + var obx = oruR01.GetPATIENT_RESULT().GetORDER_OBSERVATION().GetOBSERVATION().OBX; + obx.ValueType.Value = "FT"; + obx.GetObservationValue(0).Data = new FT(oruR01) { Value = obx5Value }; + + var xmlParser = new DefaultXMLParser(); + + // Act + var xml = xmlParser.Encode(oruR01); + xml = xml.Replace( + obx5Value, + $"{obx5Value}{obx5Value}"); + + var message = (ORU_R01)xmlParser.Parse(xml); + var parsedObx5Value = + ((FT)message.GetPATIENT_RESULT().GetORDER_OBSERVATION().GetOBSERVATION() + .OBX.GetObservationValue(0).Data).Value; + + // Assert + Assert.AreEqual($"\\H\\{obx5Value}\\.br\\{obx5Value}\\N\\", parsedObx5Value); + } + + [Test] + public void Encode_OmdO03_CorrectlyHandlesEscaping() + { + // Arrange + var er7Message = File.ReadAllText($"{TestDataDir}/omd_o03.txt"); + var xmlParser = new DefaultXMLParser(); + var pipeParser = new PipeParser(); + var message = pipeParser.Parse(er7Message); + + // Action + var xml = xmlParser.Encode(message); + + // Assert + Assert.IsTrue(xml.Contains("OMD_O03.DIET")); + } + + [Test] + public void Encode_WhenMessageContainsLongValues_DoesNotWrap() + { + // Arrange + var obx5Value = "AAAABBBB CCCCDDDD "; + obx5Value = obx5Value + obx5Value + obx5Value + obx5Value + obx5Value; + obx5Value = obx5Value + obx5Value + obx5Value + obx5Value + obx5Value; + obx5Value = obx5Value.Trim(); + + var oruR01 = new ORU_R01(); + + SetMessageHeader(oruR01, "ORU", "R01", "T"); + var obx = oruR01.GetPATIENT_RESULT().GetORDER_OBSERVATION().GetOBSERVATION().OBX; + obx.ValueType.Value = "ST"; + obx.GetObservationValue(0).Data = new ST(oruR01) { Value = obx5Value }; + + var xmlParser = new DefaultXMLParser(); + + // Act + var xml = xmlParser.Encode(oruR01); + + var message = (ORU_R01)xmlParser.Parse(xml); + var parsedObx5Value = + ((ST)message.GetPATIENT_RESULT().GetORDER_OBSERVATION().GetOBSERVATION() + .OBX.GetObservationValue(0).Data).Value; + + // Assert + Assert.AreEqual(obx5Value, parsedObx5Value); + } + + [TestCase("ABC\\H\\highlighted\\N\\EFG", "ABChighlightedEFG")] + [TestCase("\\H\\highlighted\\N\\EFG", "highlightedEFG")] + [TestCase("ABC\\H\\highlighted\\N\\", "ABChighlighted")] + [TestCase("ABC\\E\\no escape sequence\\H\\highlighted\\N\\EFG", "ABC\\no escape sequence\\no escape sequence")] + public void Encode_MessageHasEscapedSequences_IsHandledCorrectly(string obx5Value, string expectedXml) + { + // Arrange + var parserOptions = new ParserOptions { PrettyPrintEncodedXml = false }; + var xmlParser = new DefaultXMLParser(); + + var oruR01 = new ORU_R01(); + + SetMessageHeader(oruR01, "ORU", "R01", "T"); + var obx = oruR01.GetPATIENT_RESULT().GetORDER_OBSERVATION().GetOBSERVATION().OBX; + + var encodingCharacters = EncodingCharacters.FromMessage(oruR01); + + obx.ValueType.Value = "FT"; + obx.GetObservationValue(0).Data = new FT(oruR01) { Value = Escape.UnescapeText(obx5Value, encodingCharacters) }; + + // Test a couple of cases of escape sequences + // Action + var encoded = xmlParser.Encode(oruR01, parserOptions); + + // Assert + StringAssert.Contains(expectedXml, encoded); + } + + [TestCase("1234", "1234")] + [TestCase("1234\\E\\1234", "1234\\E\\1234")] + [TestCase("1234\\E\\", "1234\\E\\")] + [TestCase("1234\\E\\\\E\\", "1234\\E\\\\E\\")] + [TestCase("1234\\E\\\\.BR\\", "1234\\E\\")] + public void Encode_HandlesMessagesWithTrailingEncodedBackslash(string messageControlId, string expectedXml) + { + // Arrange + var adtA01 = new ADT_A01(); + + SetMessageHeader(adtA01, "ADT", "A01", "T"); + adtA01.MSH.MessageControlID.Value = messageControlId; + + // Test a couple of cases of escape sequences + var parserOptions = new ParserOptions { PrettyPrintEncodedXml = false }; + var xmlParser = new DefaultXMLParser(); + + // Action + var encoded = xmlParser.Encode(adtA01, parserOptions); + + // Assert + StringAssert.Contains(expectedXml, encoded); + } + + [TestCase("2.1")] + [TestCase("2.2")] + [TestCase("2.3")] + [TestCase("2.3.1")] + [TestCase("2.4")] + [TestCase("2.5")] + [TestCase("2.5.1")] + [TestCase("2.6")] + [TestCase("2.7")] + [TestCase("2.7.1")] + [TestCase("2.8")] + [TestCase("2.8.1")] + public void Encode_GenericMessage_WorksAsExpected(string version) + { + // Arrange + var xmlParser = new DefaultXMLParser(); + var type = GenericMessage.GetGenericMessageClass(version); + + var constructor = type.GetConstructor(new[] { typeof(IModelClassFactory) }); + var message = (IMessage)constructor?.Invoke(new object[] { new DefaultModelClassFactory() }); + + // Action + var document = xmlParser.EncodeDocument(message); + + // Assert + Assert.IsNotNull(document); + Assert.AreEqual($"GenericMessageV{version.Replace(".", string.Empty)}", document.DocumentElement?.LocalName); + Assert.IsNotNull(xmlParser.Encode(message)); + } + + [Test] + public void Encode_AdtA01_CanBeParsedAgain() + { + // Arrange + var er7Message = File.ReadAllText($"{TestDataDir}/adt_a03.txt"); + var xmlParser = new DefaultXMLParser(); + var pipeParser = new PipeParser(); + var message = pipeParser.Parse(er7Message); + + // Action + var document = xmlParser.EncodeDocument(message); + var decodedMessage = xmlParser.ParseDocument(document, message.Version); + + Console.WriteLine(decodedMessage.ToString()); + } + + private static void SetMessageHeader(IMessage msg, string messageCode, string messageTriggerEvent, string processingId) + { + var msh = (ISegment)msg.GetStructure("MSH"); + + var version27 = new Version("2.7"); + var messageVersion = new Version(msg.Version); + + Terser.Set(msh, 1, 0, 1, 1, "|"); + Terser.Set(msh, 2, 0, 1, 1, version27 > messageVersion ? "^~\\&" : "^~\\&#"); + Terser.Set(msh, 7, 0, 1, 1, DateTime.Now.ToString("yyyyMMddHHmmssK")); + Terser.Set(msh, 9, 0, 1, 1, messageCode); + Terser.Set(msh, 9, 0, 2, 1, messageTriggerEvent); + Terser.Set(msh, 10, 0, 1, 1, Guid.NewGuid().ToString()); + Terser.Set(msh, 11, 0, 1, 1, processingId); + Terser.Set(msh, 12, 0, 1, 1, msg.Version); + } + } +} \ No newline at end of file diff --git a/tests/NHapi.NUnit/Test23Orc.cs b/tests/NHapi.NUnit/Test23Orc.cs index 6e338b99b..b03faa773 100644 --- a/tests/NHapi.NUnit/Test23Orc.cs +++ b/tests/NHapi.NUnit/Test23Orc.cs @@ -2,8 +2,6 @@ { using global::NUnit.Framework; - using NHapi.Model.V23.Datatype; - using NHapi.Model.V23.Group; using NHapi.Model.V23.Message; [TestFixture] diff --git a/tests/NHapi.NUnit/TestData/Parser/OMD_O03.xml b/tests/NHapi.NUnit/TestData/Parser/OMD_O03.xml new file mode 100644 index 000000000..aaa0cb609 --- /dev/null +++ b/tests/NHapi.NUnit/TestData/Parser/OMD_O03.xml @@ -0,0 +1,52 @@ + + + + | + ^~\& + + TestSendingSystem + + + 200701011539 + + + OMD + O03 + OMD_O03 + + + 2.5 + + 123 + + + + + 1 + + hellin + + PI + + + + Doe + + John + + + + + + + S + + breakfast + + + 320^1/2 HAM SANDWICH^99FD8 + + + + + \ No newline at end of file diff --git a/tests/NHapi.NUnit/TestData/Parser/adt_a03.txt b/tests/NHapi.NUnit/TestData/Parser/adt_a03.txt new file mode 100644 index 000000000..e7e2b7a1a --- /dev/null +++ b/tests/NHapi.NUnit/TestData/Parser/adt_a03.txt @@ -0,0 +1,5 @@ +MSH|^~\&|IRIS|SANTER|AMB_R|SANTER|200803051508||ADT^A03|263206|P|2.5 +EVN||200803051509||||200803031508 +PID|||5520255^^^PK^PK~ZZZZZZ83M64Z148R^^^CF^CF~ZZZZZZ83M64Z148R^^^SSN^SSN^^20070103^99991231~^^^^TEAM||ZZZ^ZZZ||19830824|F||||||||||||||||||||||N +PV1||I|6402DH^^^^^^^^MED. 1 - ONCOLOGIA^^OSPEDALE MAGGIORE DI LODI&LODI|||^^^^^^^^^^OSPEDALE MAGGIORE DI LODI&LODI|13936^TEST^TEST||||||||||5068^TEST2^TEST2||2008003369||||||||||||||||||||||||||200803031508 +PR1|1||1111^Mastoplastica|Protesi|20090224|02| \ No newline at end of file diff --git a/tests/NHapi.NUnit/TestData/Parser/adt_a17.xml b/tests/NHapi.NUnit/TestData/Parser/adt_a17.xml new file mode 100644 index 000000000..6920d6e89 --- /dev/null +++ b/tests/NHapi.NUnit/TestData/Parser/adt_a17.xml @@ -0,0 +1,221 @@ + + + + | + ^~\& + + HIS + + + 050101 + + + CUIDADOS + + + 050101 + + 20080427092114 + + ADT + A17 + ADT_A17 + + MENSAJE_EJEMPLO_ADT_A17_1 + P + 2.5 + AL + ER + + + 20080427092100 + 20080427092100 + + + 1 + + 12345679 + MI + NNESP + + ESP + ISO3166 + + + + + AST12345679 + MS + HC + + ESP + ISO3166 + + + + 100000 + HIS + PI + + 050101 + 99CENTROMICASA + + + + + SaEZ + + ALBERTO + + + + TORRES + + + + 19750322 + + M + + + C + NiNa Bonita + 78 + + 5 + 051159 + 5 + 5291 + ESP + H + Maello + + + + PRN + PH + 921787865 + + + ESP + EspaNa + ISO3166 + + + + + 1 + I + + UE6D + 117- + 117-1 + + 050101 + + + + 11111111111111 + HOS + VN + + 050101 + 99CENTROMICASA + + + + + 2 + + + 2222222l + MI + NNESP + + ESP + ISO3166 + + + + + AST2222222 + MS + HC + + ESP + ISO3166 + + + + + 200000 + HIS + PI + + 050101 + 99CENTROMICASA + + + + + SWAP2 + + SWAP1 + + + + SWAP3 + + + + 19750322 + + M + + + C + NiNa Bonita + 78 + + 4 Izquierda + 051159 + 5 + 5291 + ESP + H + Maello + + + + PRN + PH + 921787865 + + + ESP + EspaNa + ISO3166 + + + + 2 + I + + UE6D + 217- + 217-2 + + 050101 + + + + 2222222222222222 + HOS + VN + + 050101 + 99CENTROMICASA + + + + \ No newline at end of file diff --git a/tests/NHapi.NUnit/TestData/Parser/critical_response_data.xml b/tests/NHapi.NUnit/TestData/Parser/critical_response_data.xml new file mode 100644 index 000000000..b228fc5dc --- /dev/null +++ b/tests/NHapi.NUnit/TestData/Parser/critical_response_data.xml @@ -0,0 +1,20 @@ + + + + | + ^~\& + LABMI1 + DMCRES + + 19951010134000 + + + ORU + R01 + + LABMI1199510101340007 + D + 2.2 + AL + + \ No newline at end of file diff --git a/tests/NHapi.NUnit/TestData/Parser/ed_issue.xml b/tests/NHapi.NUnit/TestData/Parser/ed_issue.xml new file mode 100644 index 000000000..ed375547b --- /dev/null +++ b/tests/NHapi.NUnit/TestData/Parser/ed_issue.xml @@ -0,0 +1,364 @@ + + + + | + ^~\& + + APPNAME + + + VENDOR NAME + + + CLINIC APPLICATION + + + CLINIC ID + + + 20100723170708 + + + ORU + R01 + + 12345 + + P + + + 2.5 + + + + + + + MODEL:xxx/SERIAL:xxx + + STJ + + U + + + + Doe + + John + Adams + + + 197903110920 + + M + + + Street + + City + 06531 + Country + + + + + 1 + R + + DoctorID + + + 123456 + + + + + + + 1 + + 123456 + + + Remote Follow-up + + + 20040328134623 + + + 20040328134623 + + + 20040328134623 + + F + + + 1 + L + Comment + + + + 1 + ST + + 257 + MDC-IDC_SYSTEM_STATUS + MDC_IDC + + 1 + + m + + L + F + + 20070422170125 + + + LastFU + Since Last Follow-up Aggregate + + + + + + 2 + ED + + 18750-0 + Cardiac Electrophysiology Report + LN + + + Application + PDF + Base64 + + + JVBERi0xLjMKJeLjz9MKCjEgMCBvYmoKPDwgL1R5cGUgL0NhdGFsb2cKL091dGxpbmVzID + IgMCBS + + Ci9QYWdlcyAzIDAgUgovT3BlbkFjdGlvbiA4IDAgUiA+PgplbmRvYmoKMiAwIG9iago8PCAvVHlw + + ZSAvT3V0bGluZXMgL0NvdW50IDAgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzCi9L + + aWRzIFs2IDAgUgpdCi9Db3VudCAxCi9SZXNvdXJjZXMgPDwKL1Byb2NTZXQgNCAwIFIKL0ZvbnQg + + PDwgCi9GMSAxMSAwIFIKL0YyIDEyIDAgUgovRjMgMTcgMCBSID4+Ci9YT2JqZWN0IDw8IAovSTEg + + OSAwIFIgPj4KPj4KL01lZGlhQm94IFswLjAwMCAwLjAwMCA1OTUuMjgwIDg0MS44OTBdCiA+Pgpl + + bmRvYmoKNCAwIG9iagpbL1BERiAvVGV4dCAvSW1hZ2VDIF0KZW5kb2JqCjUgMCBvYmoKPDwKL0Ny + + ZWF0b3IgKERPTVBERiBDb252ZXJ0ZXIpCi9DcmVhdGlvbkRhdGUgKDIwMTAtMDItMTUpCj4+CmVu + + ZG9iago2IDAgb2JqCjw8IC9UeXBlIC9QYWdlCi9QYXJlbnQgMyAwIFIKL0Fubm90cyBbIDEzIDAg + + UiAxNSAwIFIgXQovQ29udGVudHMgNyAwIFIKPj4KZW5kb2JqCjcgMCBvYmoKPDwgL0ZpbHRlciAv + + RmxhdGVEZWNvZGUKL0xlbmd0aCA5MTcgPj4Kc3RyZWFtCnicnVbZbttGFH3XV9y3xoA8nn1IFQgg + + 23HSBC1cS4Uf6jyMpZE5KReXS4wG+Zi+9i97SYuLRSl2AgMUfMR77pm7nNGEEUopDJ/53YTBA3B4 + + DxQ+wZ/wET/XE8abb2UgiA4ZKMMIDygIqQkLQ8gdbCZ/T0LdvFX/iYDImvQxzoSMyJDCKpmc/MLg + + PJv8PqGP7w6emPt02YUYQxgPYLmGkwvWoLDcALy6rG5jX0RuDVkKR7D8BG+WdVwgiaFBE8cZfYzj + + g7hfbVHYVVQVriwLmMdfIucTl/9UwLkvnC0cXDn8zFcRnLm0dPmAWwQBCbg8LApuXrWvD0/EBucS + + WB2m9GGOqCzvZycniV3nK5LcRSSy+Webr4lbV4fJr94iqLBlFFvWtkuECstf51LESAYJKCWJYLRD + + Ylgc7IBSgij9jRbcHD132LaJIiShoi1D2BK8yxLXU+yqZzhcqg4OSIiSExCaGCU64FvaRT2C4mBi + + eF13fpPlVQI2XcPiev7bDJaRg8scu5+WDXpRlRWOdLaBxcoj6Dd+BdfuFs6yJKlSX3pX9PI1ryX0 + + z0MrJLYl4UToEJQOmxXaiq8XaO+ZxiB2vCuvaLiwvbiYNVuLxIjwLcK3yI6AGDBlR8RGROy7iHYF + + vIxpjAw1tUw7IvcI2Keon0IdojUg3AyDAC7bafiuURjYQcurKbqaGfM+PzUDEgyTXLUkOz63sqVH + + mzu3pZv9OwzGEwvFm2gh5XhFOaVmnEyhL+hW8fD1s9jmf8GyOfIHn2ZF5EuLA3zTEN0ckadrUzzY + + dAYl1up+UKtNV6uiL8ADFmDVF4DsqaMyuJ+KjVWd5t5t4NRnPsXcybOOodCwtJY4MvV9I2QH1I6B + + +UJc0EAezvczBDNgWhwzvN+GOhUJ0LmVwn7TPcV+erW8danLbQzvsuIeqxhP4dqmdzBPbqvYlln+ + + D5xZLNPjJQOLypcODGfnU8wDl9iHxKawKHPnyimcZkWZpdNh1TjHSxjNXeGCGLXHnp/KoZwxOYU/ + + FvMn88OIxvlRDC2e8THH0sXuPspSNwPNzLHh+liEgYGvcGFXhU98PPhGMsogy7v/jWHBMBlOnanr + + TrF+Wo2T/QfvbVpZrAybAqdo8fM4hit/F6H++lbOP+N1/6OOK+u21z9aXui47W8dnKE6rHeyFul9 + + q6XuHKgHeiOTQbhLtEVeSrQr4GVMY2SgqWPaEblHwB5F/wMtHGbMCmVuZHN0cmVhbQplbmRvYmoK + + CjggMCBvYmoKWzYgMCBSIC9GaXRdCmVuZG9iagoKOSAwIG9iago8PAovVHlwZSAvWE9iamVjdAov + + U3VidHlwZSAvSW1hZ2UKL1dpZHRoIDIwMAovSGVpZ2h0IDgwCi9GaWx0ZXIgL0ZsYXRlRGVjb2Rl + + Ci9EZWNvZGVQYXJtcyA8PCAvUHJlZGljdG9yIDE1IC9Db2xvcnMgMSAvQ29sdW1ucyAyMDAgL0Jp + + dHNQZXJDb21wb25lbnQgOD4+Ci9Db2xvclNwYWNlICBbIC9JbmRleGVkIC9EZXZpY2VSR0IgMzIg + + MTAgMCBSIF0KL0JpdHNQZXJDb21wb25lbnQgOAovTGVuZ3RoIDIwMzEgPj4Kc3RyZWFtCmje7Zlp + + l6soEIZNOp2OccGoGBNc+P+/cqhiK1zSduaeM33u6Ic0IhT1SBXg25H8S65oB9lBdpAdZAf5/SB1 + + fXflZ133r1rW7xh9v48DqaPo0xSvUXQxxbsvSnk5RFF0OD/xwUmVo49M+j7e8/snPIw+YbBLFLkB + + anIrT9FpYrSO3FVLX1YeBDehI0sg0U2/6oP3/qw62BafxtShRkB9WfijL8rMjZp9C0KNbgWhfZZB + + Dr0ewYE8jTemwbmXz8vhhG3Ayu1kpwF9N2/opgxl2PJ7kMCournW+uohNmv1GuHPc3JD+yyDRGfr + + kwG5RMdzdLRl3bYHf22Lu/PqfMTeODkHXX3Pvg2twGhNw5MOGd4EfZZzBAz1B/ir3VTl7G6tq/4+ + + wUggmc7Pq5nQzM1hOP4ayJ1a2Qhyf7FqgZVTdOw/lT8WJAPfTsZnSIvzrXf5crx6c2fllKK+6vJh + + 4oyOlusSSGDUh1b/CiToswyi0vwEKW9BjtEXvuEnyeFPfN39B5SPX0+bSjcgOJLViIxPUniW7NRo + + TRuugwR9lkFgEYX3b0BqjeCC//l1BAvazwzXjsPNpJLGyX4MQo1uBQkdWQSB1ah3ICrSLur6MMGP + + Jq7KxNXc3MzirILqBA0PaHoeWi/3EWJ0Y47MHVkAueNeokGe4S5krv5I3oSO+yx456+TPXOZerAT + + 7Yz+BGTiyAxE1tKBqFd7wuuIkdN/6Xn5gP7ZzW4ZNcTeUTfUvh3s8vu8zEGekfH/hryB0Y0gQZ9V + + EOlA3NoFjzKMuutT9nh+gbVaodwx6m40pp/BhniegcB2+wVmDhiw1Gi4Ia6DBH02gFx8agB777L2 + + qLbdD3tzpcFu2F2owcxMQeAA5B4GRsMjyipI2Od7kP7oYzjTUXFGH85629Pr7w2X9czvJwivD436 + + VDcFkf3ZnyhDo9tAJo68d4y/k8N5D+ee987m4Rn/vv3E/6LP/oW4g+wgO8gO8mtABs6HV/f2ann7 + + J1zgXJifPw0i4li8urcXi9nWYco4HteexTE3P/8VSMWqrcOwuJAvQRhr/zuQH1wsbl+D/MEcGThj + + rBqs4yox9NXifVeyQtOMjWomXI7AT4PvU7gmsi1YiZ4LzmVXVjLHyIIWDQkxaNZpEJ0jQ8WYyUdn + + QgpVyXQ3P/bMlgPpYn21BkSYe5UHqtgk5pl8YClubI6onxwqqtZ2lx1WYDDxOO4S1QxvKqxO7OyO + + TPfwOfKIrW1iotGVSac4E1s7FljKh4UZSXPOVU4mI52RHFwDJsZV1wRtsYcoMNgMSJxUFYyQYndl + + KY/TVnD9olV1nFYSAAdVI1qWkgUg5VUae5A0LsSjjIfAxBAXnDN4oapH/hBV0sI7SZSthKw2HgSn + + qTWOC5cdHH8LPbBQP+lIJgNBOuwH9dikxRrFoN4JN5NnjI12HIlcTJXH3IPE8UM3oCZ0B2V5UGNV + + +vmgrdLkJSAPzpsODLrnKipK3x7/JNovFQSUxmYsNin0CjVCmZNMVqOnjY+FRjsrOzojSYV11IQc + + WpWorX6LcfnQfRO0kXvrPkcSkxMeRIVRIScgsbvEGghzTTiAeNcxi1hn90F4F+E+gj6kDxmYaP2A + + MHsqprCvy+ApSBInrdBhaRxX3fJxDsLMajasg6SmifDe6jlplCfJ4EBG89rdhji2kMQPakJNmEqM + + R6l9eJSKtcKJtmvqBETo+JQexHEEIDkGG9nZ5yBVnPrjRwCiZ5m7ERvThO7sIxgkJgyvTwe19kFk + + Dyv7iNArZ+NBSlzxpiCtaQeMKyAqFzCz0i4EETlYSV1Yq/WqGcfGr1pj2mJaM2rCBCeuk4zrhQD6 + + ggND3s5XLZVoJaxyFqQJ9hEHgosmSxFnBQSGSmBzKUOQwvQc/LtD+w6kMf1aagKWiIrDIg1RFjOG + + diGboJiP82QHiFQ4EL4CIrnNyFUQ+UhNUgYgo+9pSKBZQXKkdf2ICSwxXLUELgLlaN3VxdlZaxDd + + tmNN933DQYgtPTsxrPbzRdJqFGKcF/cvxB1kB9lBdpC/DORfizu/BeRtPeH9N/DLQN5XVP4+ECvc + + hEoL0V9otZeOUE8dK2YbF63xR9VNNbe2sLIO0X2gv9WaBo7fbUM4GvFsC4gVbgKlxesvQTWRjvCL + + LMczrhGKQFFQX3Go55T0gKgVnlSrCk4YolqTsN+0wWjOs80gKNwESovXX0IBxktHMLj6lGFIp79o + + OgABpSc28oKeW3UaL6BTg9JFKRS+6e+0Jjibpwy+6ulozrPNIPjpGSgtTn+ZCDBeOkJHSq0Q4Rdl + + 22JopfpbjtOPXhTvKvyIwzlOplqTy5FgNCopbQPhM6XF6S8TAcZLRzAYM2Pzac7SqtQHR2dmqoKq + + 8KttyQm+aQWgIIOUE6XF6S9hNZGO7Md+qHQvgJCy8JrSCkgwGp8rDd+AuJkhSovRX8JqIh05ACvD + + rINUBKQ0xtZB/GgzJWYbyFxpAf0lqKbSkZ8Ju7CMyyAFpjY+HmPyT4ZFkMCJN0ECpcXrL7SaSkce + + RBkoBrXw5OMiiGqXC7V/wIqglaaxLBdACjmOoRPvghClhegvgQBDpCOSG7kJ68ciiPmnAkYYpF7O + + EsQJQXKzj9DR3gWhSgvRX2g1kY5okqPck4vl0DK6TooL6Vg6GTgEwWWkC0f7IQi9VkQXUl6RjoQY + + tupIa03dGBO5Z/+w2kF2kB1kB9lBdpD/E8g/QZSt+wplbmRzdHJlYW0KZW5kb2JqCgoxMCAwIG9i + + ago8PCAvRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDExMCA+PgpzdHJlYW0KeJwBYwCc/2MG + + EW0WIHclL4A1PopETZRUXJ5ka6hzerGCiJaWlrqSlp2dnaOjo8ShpaqqqrCwsLe3t86xtL6+vsTE + + xNjBw8rKytHR0eLQ0tfX197e3uvg4eXl5evr6/Xv8PLy8vj4+P///6g8Qs0KZW5kc3RyZWFtCmVu + + ZG9iagoKMTEgMCBvYmoKPDwgL1R5cGUgL0ZvbnQKL1N1YnR5cGUgL1R5cGUxCi9OYW1lIC9GMQov + + QmFzZUZvbnQgL0hlbHZldGljYQovRW5jb2RpbmcgL1dpbkFuc2lFbmNvZGluZwo+PgplbmRvYmoK + + MTIgMCBvYmoKPDwgL1R5cGUgL0ZvbnQKL1N1YnR5cGUgL1R5cGUxCi9OYW1lIC9GMgovQmFzZUZv + + bnQgL0hlbHZldGljYS1Cb2xkT2JsaXF1ZQovRW5jb2RpbmcgL1dpbkFuc2lFbmNvZGluZwo+Pgpl + + bmRvYmoKMTMgMCBvYmoKPDwgL1R5cGUgL0Fubm90Ci9TdWJ0eXBlIC9MaW5rCi9BIDE0IDAgUgov + + Qm9yZGVyIFswIDAgMF0KL0ggL0kKL1JlY3QgWyAzOTYuMTU2MCA3NzcuMTI4NCA1NTMuNTYwMCA3 + + OTEuMDAwNCBdCj4+CmVuZG9iagoxNCAwIG9iago8PCAvVHlwZSAvQWN0aW9uCi9TIC9VUkkKL1VS + + SSAoaHR0cDovL21hZHJjLm1naC5oYXJ2YXJkLmVkdSkKPj4KZW5kb2JqCjE1IDAgb2JqCjw8IC9U + + eXBlIC9Bbm5vdAovU3VidHlwZSAvTGluawovQSAxNiAwIFIKL0JvcmRlciBbMCAwIDBdCi9IIC9J + + Ci9SZWN0IFsgMTIuMDAwMCA3MzkuOTUwMCAzNi4wMDMwIDc1MC4zNTQwIF0KPj4KZW5kb2JqCjE2 + + IDAgb2JqCjw8IC9UeXBlIC9BY3Rpb24KL1MgL1VSSQovVVJJIChodHRwOi8vbWFkcmMubWdoLmhh + + cnZhcmQuZWR1LykKPj4KZW5kb2JqCjE3IDAgb2JqCjw8IC9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9U + + eXBlMQovTmFtZSAvRjMKL0Jhc2VGb250IC9IZWx2ZXRpY2EtQm9sZAovRW5jb2RpbmcgL1dpbkFu + + c2lFbmNvZGluZwo+PgplbmRvYmoKeHJlZgowIDE4CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAw + + MDAxNSAwMDAwMCBuIAowMDAwMDAwMDk4IDAwMDAwIG4gCjAwMDAwMDAxNDQgMDAwMDAgbiAKMDAw + + MDAwMDM0NyAwMDAwMCBuIAowMDAwMDAwMzg0IDAwMDAwIG4gCjAwMDAwMDA0NjAgMDAwMDAgbiAK + + MDAwMDAwMDU0OSAwMDAwMCBuIAowMDAwMDAxNTM5IDAwMDAwIG4gCjAwMDAwMDE1NjggMDAwMDAg + + biAKMDAwMDAwMzg2OCAwMDAwMCBuIAowMDAwMDA0MDUyIDAwMDAwIG4gCjAwMDAwMDQxNjAgMDAw + + MDAgbiAKMDAwMDAwNDI4MCAwMDAwMCBuIAowMDAwMDA0NDA4IDAwMDAwIG4gCjAwMDAwMDQ0ODgg + + MDAwMDAgbiAKMDAwMDAwNDYxNCAwMDAwMCBuIAowMDAwMDA0Njk1IDAwMDAwIG4gCnRyYWlsZXIK + + PDwKL1NpemUgMTgKL1Jvb3QgMSAwIFIKL0luZm8gNSAwIFIKPj4Kc3RhcnR4cmVmCjQ4MDgKJSVF + T0YK + + + F + + 20070422170125 + + + + + + 3 + CE + + 257 + MDC-IDC_SYSTEM_STATUS + MDC_IDC + + 1 + + T57000 + GALLBLADDER + SNM + + + m + + L + F + + 20070422170125 + + + LastFU + Since Last Follow-up Aggregate + + + + + + \ No newline at end of file diff --git a/tests/NHapi.NUnit/TestData/Parser/extracmp_xml.xml b/tests/NHapi.NUnit/TestData/Parser/extracmp_xml.xml new file mode 100644 index 000000000..25a70604f --- /dev/null +++ b/tests/NHapi.NUnit/TestData/Parser/extracmp_xml.xml @@ -0,0 +1,33 @@ + + + + | + ^~\& + + OLIS + X500 + HD.4 + HD.5 + + + 2.16.840.1.113883.3.59.3:0947 + ISO + + + 20130819093140-0400 + + + ERP + Z99 + ERP_R09 + + a9aa9d25-d4c0-4e1d-8e4f-b79d16ef5179 + + T + + + 2.3.1 + + 8859/1 + + \ No newline at end of file diff --git a/tests/NHapi.NUnit/TestData/Parser/get_ack_id.xml b/tests/NHapi.NUnit/TestData/Parser/get_ack_id.xml new file mode 100644 index 000000000..cb4138e41 --- /dev/null +++ b/tests/NHapi.NUnit/TestData/Parser/get_ack_id.xml @@ -0,0 +1,46 @@ + + + + + | + ^~/& + + MPI + ISO + + + HealthLink + ISO + + + UHN Vista + ISO + + + UHN + ISO + + 200204292049 + + RSP + K22 + RSP_K22 + + 200204292049100799 + + P + + + 2.4 + + Q22 + + + AA + 876 + + + OK + + Q22 + \ No newline at end of file diff --git a/tests/NHapi.NUnit/TestData/Parser/get_version.xml b/tests/NHapi.NUnit/TestData/Parser/get_version.xml new file mode 100644 index 000000000..e760491fd --- /dev/null +++ b/tests/NHapi.NUnit/TestData/Parser/get_version.xml @@ -0,0 +1,96 @@ + + + + + + | + ^~\& + + UHN Vista + ISO + + + UHN + ISO + + + MPI + ISO + + + HealthLink + ISO + + 20020429132718.734-0400 + + QBP + Q22 + QBP_Q21 + + 855 + + P + + + 2.4 + + Q22 + + + + Q22 + Find Candidates + HL7nnnn + + + + @PID.3.1 + 9583518684 + + + @PID.3.4.1 + CANON + + + @PID.5.1.1 + ECG-Acharya + + + @PID.5.2 + Nf + + + @PID.5.7 + L + + + @PID.7 + 197104010000 + + + @PID.8 + M + + + 100 + + + TTH + + + 13831 + ULTIuser2 + 234564 + R&H Med + + + I + + 100 + RD + + + R + + + \ No newline at end of file diff --git a/tests/NHapi.NUnit/TestData/Parser/omd_o03.txt b/tests/NHapi.NUnit/TestData/Parser/omd_o03.txt new file mode 100644 index 000000000..41f584110 --- /dev/null +++ b/tests/NHapi.NUnit/TestData/Parser/omd_o03.txt @@ -0,0 +1 @@ +MSH|^~\&|DIETOOLS|1^DOMINION|JARA||20101027181706||OMD^O03^OMD_O03|20101027181706|P|2.5|||ER|AL PID|1||CIPNUM^^^CAEX^CIP^^^^EX&&ES-EX||DUMAS^VICTOR HUGO|ZAPATA|19740325|1 PV1|1|I|^^51C302-1^^^^^^^0005&51UHP31&UH TERCERA 1 HSPA&TIPOUOENF||||||||||||||||100002739^005^^^^^^^^0005&APARATO DIGESTIVO&5DIG&TIPOUOSERV| ORC|XO||||||||20101117|1^^HERNAME TQ1|1||CE||||20101117 ODS|D||PAN4^PEDIATRICA 1| ODS|P||EVENTO^LACT| ODS|P||CARACTERISTICA^SIN SAL| ODS|P||CARACTERISTICA^DIABETICO| \ No newline at end of file diff --git a/tests/NHapi.NUnit/TestData/Parser/parse_and_encode.xml b/tests/NHapi.NUnit/TestData/Parser/parse_and_encode.xml new file mode 100644 index 000000000..70d504b24 --- /dev/null +++ b/tests/NHapi.NUnit/TestData/Parser/parse_and_encode.xml @@ -0,0 +1,20 @@ + + + + | + ^~\& + LABMI1 + DMCRES + + 19951010134000 + + + ORU + R01 + + LABMI1199510101340007 + D + 2.2 + AL + + \ No newline at end of file diff --git a/tests/NHapi.NUnit/TestData/Parser/parse_and_encode_with_ns.xml b/tests/NHapi.NUnit/TestData/Parser/parse_and_encode_with_ns.xml new file mode 100644 index 000000000..59898a249 --- /dev/null +++ b/tests/NHapi.NUnit/TestData/Parser/parse_and_encode_with_ns.xml @@ -0,0 +1,20 @@ + + + + | + ^~\& + LABMI1 + DMCRES + + 19951010134000 + + + ORU + R01 + + LABMI1199510101340007 + D + 2.2 + AL + + \ No newline at end of file