Skip to content

Commit 01b7ea5

Browse files
committed
Add support for multiple UnexpectedSegmentBehaviour options
1 parent 0f92aec commit 01b7ea5

File tree

5 files changed

+200
-41
lines changed

5 files changed

+200
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ All notable changes to this project will be documented in this file.
88
## [3.1.0] - TBC
99

1010
### Enhancements
11-
- [#251](https://github.com/nHapiNET/nHapi/pull/251) Fix concurrency issues with `PipeParser`, `StructureDefinition`, `GenericMessage` and `Escape`.
11+
- [#254](https://github.com/nHapiNET/nHapi/pull/254) Add `UnexpectedSegmentBehaviour` Options, ported from hapi.
1212
- [#250](https://github.com/nHapiNET/nHapi/pull/250) Add new options `DefaultObx2Type` and `InvalidObx2Type` to `ParserOptions`, ported from nhapi. (fixes [#63](https://github.com/nHapiNET/nHapi/issues/63))
1313
- [#240](https://github.com/nHapiNET/nHapi/pull/240) Add support for `NonGreedyMode` by introducing `ParserOptions` ported from hapi. (fixes [#65](https://github.com/nHapiNET/nHapi/issues/65) [#232](https://github.com/nHapiNET/nHapi/issues/232))
1414

src/NHapi.Base/Parser/MessageIterator.cs

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -55,28 +55,45 @@ public MessageIterator(
5555
this.parserOptions = parserOptions;
5656
}
5757

58-
/// <summary> <p>Returns the next node in the message. Sometimes the next node is
58+
/// <summary>
59+
/// <para>
60+
/// Returns the next node in the message. Sometimes the next node is
5961
/// ambiguous. For example at the end of a repeating group, the next node
6062
/// may be the first segment in the next repetition of the group, or the
6163
/// next sibling, or an undeclared segment locally added to the group's end.
62-
/// Cases like this are disambiguated using getDirection(), which returns
64+
/// Cases like this are disambiguated using <see cref="Direction"/>, which returns
6365
/// the name of the structure that we are "iterating towards".
6466
/// Usually we are "iterating towards" a segment of a certain name because we
6567
/// have a segment string that we would like to parse into that node.
66-
/// Here are the rules: </p>
67-
/// <ol><li>If at a group, next means first child.</li>
68-
/// <li>If at a non-repeating segment, next means next "position"</li>
69-
/// <li>If at a repeating segment: if segment name matches
70-
/// direction then next means next rep, otherwise next means next "position".</li>
71-
/// <li>If at a segment within a group (not at the end of the group), next "position"
72-
/// means next sibling</li>
73-
/// <li>If at the end of a group: If name of group or any of its "first
68+
/// Here are the rules:
69+
/// </para>
70+
/// <list type="bullet">
71+
/// <item>
72+
/// <description>If at a group, next means first child.</description>
73+
/// </item>
74+
/// <item>
75+
/// <description>If at a non-repeating segment, next means next "position"</description>
76+
/// </item>
77+
/// <item>
78+
/// <description>If at a repeating segment: if segment name matches
79+
/// direction then next means next rep, otherwise next means next "position".</description>
80+
/// </item>
81+
/// <item>
82+
/// <description>If at a segment within a group (not at the end of the group), next "position"
83+
/// means next sibling</description>
84+
/// </item>
85+
/// <item>
86+
/// <description>If at the end of a group: If name of group or any of its "first
7487
/// descendants" matches direction, then next position means next rep of group. Otherwise
7588
/// if direction matches name of next sibling of the group, or any of its first
7689
/// descendants, next position means next sibling of the group. Otherwise, next means a
77-
/// new segment added to the group (with a name that matches "direction"). </li>
78-
/// <li>"First descendants" means first child, or first child of the first child,
79-
/// or first child of the first child of the first child, etc. </li> </ol>
90+
/// new segment added to the group (with a name that matches "direction").</description>
91+
/// </item>
92+
/// <item>
93+
/// <description>"First descendants" means first child, or first child of the first child,
94+
/// or first child of the first child of the first child, etc.</description>
95+
/// </item>
96+
/// </list>
8097
/// </summary>
8198
public virtual object Current
8299
{
@@ -233,26 +250,24 @@ private void AddNonStandardSegmentAtCurrentPosition()
233250
Log.Debug(
234251
$"Creating non standard segment {direction} on group: {GetCurrentPosition().StructureDefinition.Parent.Name}");
235252

236-
// TODO: make configurable like hapi?
237-
// switch (message.getParser().getParserConfiguration().getUnexpectedSegmentBehaviour())
238-
// {
239-
// case ADD_INLINE:
240-
// default:
241-
// parentDefinitionPath = new ArrayList<>(myCurrentDefinitionPath.subList(0, myCurrentDefinitionPath.size() - 1));
242-
// parentStructure = (IGroup)navigateToStructure(parentDefinitionPath);
243-
// break;
244-
// case DROP_TO_ROOT:
245-
// parentDefinitionPath = new ArrayList<>(myCurrentDefinitionPath.subList(0, 1));
246-
// parentStructure = myMessage;
247-
// myCurrentDefinitionPath = myCurrentDefinitionPath.subList(0, 2);
248-
// break;
249-
// case THROW_HL7_EXCEPTION:
250-
// throw new Error(new HL7Exception("Found unknown segment: " + myDirection));
251-
// }
252-
253-
// default hapi behaviour
254-
var parentDefinitionPath = new List<Position>(currentDefinitionPath.GetRange(0, currentDefinitionPath.Count - 1));
255-
var parentStructure = (IGroup)NavigateToStructure(parentDefinitionPath);
253+
List<Position> parentDefinitionPath;
254+
IGroup parentStructure;
255+
256+
switch (this.parserOptions.UnexpectedSegmentBehaviour)
257+
{
258+
case UnexpectedSegmentBehaviour.AddInline:
259+
default:
260+
parentDefinitionPath = new List<Position>(currentDefinitionPath.GetRange(0, currentDefinitionPath.Count - 1));
261+
parentStructure = (IGroup)NavigateToStructure(parentDefinitionPath);
262+
break;
263+
case UnexpectedSegmentBehaviour.DropToRoot:
264+
parentDefinitionPath = new List<Position>(currentDefinitionPath.GetRange(0, 1));
265+
parentStructure = this.message;
266+
currentDefinitionPath = currentDefinitionPath.GetRange(0, 2);
267+
break;
268+
case UnexpectedSegmentBehaviour.ThrowHl7Exception:
269+
throw new HL7Exception($"Found unknown segment: {direction}");
270+
}
256271

257272
// Current position within parent
258273
var currentPosition = GetCurrentPosition();

src/NHapi.Base/Parser/ParserOptions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public ParserOptions()
66
{
77
DefaultObx2Type = null;
88
InvalidObx2Type = null;
9+
UnexpectedSegmentBehaviour = UnexpectedSegmentBehaviour.AddInline;
910
NonGreedyMode = false;
1011
}
1112

@@ -49,6 +50,12 @@ public ParserOptions()
4950
/// </example>
5051
public string InvalidObx2Type { get; set; }
5152

53+
/// <summary>
54+
/// Gets or Sets the behaviour to use when parsing a message and a nonstandard segment is found.
55+
/// </summary>
56+
/// <remarks>The default value is <see cref="Parser.UnexpectedSegmentBehaviour.AddInline"/>.</remarks>
57+
public UnexpectedSegmentBehaviour UnexpectedSegmentBehaviour { get; set; }
58+
5259
/// <summary>
5360
/// If set to <c>true</c> (default is <c>false</c>), pipe parser will be put in non-greedy mode. This setting
5461
/// applies only to <see cref="PipeParser"/> and will have no effect on <see cref="XMLParser"/>.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace NHapi.Base.Parser
2+
{
3+
/// <summary>
4+
/// Defines the behaviour to use when an unexpected segment is discovered while parsing a message.
5+
/// </summary>
6+
/// <remarks>See <see cref="ParserOptions.UnexpectedSegmentBehaviour"/>.</remarks>
7+
public enum UnexpectedSegmentBehaviour
8+
{
9+
/// <summary>
10+
/// Add the segment as a <see cref="NHapi.Base.Model.IMessage.AddNonstandardSegment(string, int)"/>
11+
/// at the current location, even if the current location is in a child group within the message.
12+
/// </summary>
13+
/// <remarks>This is the default.</remarks>
14+
AddInline,
15+
16+
/// <summary>
17+
/// Return the parser back to the root of the message (even if the last segment was in a group) and add
18+
/// the unexpected segment as a <see cref="NHapi.Base.Model.IMessage.AddNonstandardSegment(string, int)"/>.
19+
/// </summary>
20+
DropToRoot,
21+
22+
/// <summary>
23+
/// Throw an <see cref="HL7Exception"/>
24+
/// </summary>
25+
ThrowHl7Exception,
26+
}
27+
}

tests/NHapi.NUnit/Parser/PipeParserTests.cs

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ public void TestOBR5RepeatingValuesMessage()
4040
public void TestSpecialCharacterEncoding()
4141
{
4242
var parser = new PipeParser();
43-
var oru = new ORU_R01();
44-
oru = (ORU_R01)parser.Parse(GetMessage());
43+
var oru = (ORU_R01)parser.Parse(GetMessage());
4544

4645
var data = (FT)oru.GetPATIENT_RESULT(0).GetORDER_OBSERVATION(0).GetOBSERVATION(0).OBX.GetObservationValue(0).Data;
4746
Assert.AreEqual(@"This\.br\is\.br\A Test", data.Value);
@@ -311,7 +310,7 @@ public void MoreGreedyMode()
311310
}
312311

313312
/// <summary>
314-
/// The folllwing 4 tests are ported from hapi
313+
/// The following 4 tests are ported from hapi
315314
/// https://github.com/hapifhir/hapi-hl7v2/blob/master/hapi-examples/src/main/java/ca/uhn/hl7v2/examples/ParseInvalidObx2Values.java.
316315
/// </summary>
317316
[Test]
@@ -330,7 +329,7 @@ public void Obx5DataTypeIsSetFromObx2_WhenObx2IsEmpty_Hl7ExceptionIsThrown()
330329

331330
Assert.AreEqual(
332331
"OBX-5 is valued, but OBX-2 is not. A datatype for OBX-5 must be specified using OBX-2.",
333-
exception.Message);
332+
exception?.Message);
334333
}
335334

336335
[Test]
@@ -375,7 +374,7 @@ public void Obx5DataTypeIsSetFromObx2_WhenObx2IsAnInvalidType_Hl7ExceptionIsThro
375374

376375
Assert.AreEqual(
377376
"'BAD' in record 1 is invalid for version 2.3: Segment: OBX Field #2",
378-
exception.Message);
377+
exception?.Message);
379378
}
380379

381380
[Test]
@@ -409,7 +408,7 @@ public void GetCriticalResponseDataFromValidMessage()
409408
{
410409
var parser = new PipeParser();
411410

412-
var parsed = parser.GetCriticalResponseData(GetMessage()) as NHapi.Model.V231.Segment.MSH;
411+
var parsed = parser.GetCriticalResponseData(GetMessage()) as Model.V231.Segment.MSH;
413412
Assert.NotNull(parsed);
414413
Assert.AreEqual("|", parsed.FieldSeparator.Value);
415414
Assert.AreEqual(@"^~\&", parsed.EncodingCharacters.Value);
@@ -428,7 +427,118 @@ public void GetCriticalResponseData_FailToParseInvalidMessage()
428427
var parser = new PipeParser();
429428

430429
var exception = Assert.Throws<HL7Exception>(() => parser.GetCriticalResponseData(invalidMessage));
431-
Assert.True(exception.Message.Contains("Can't parse critical fields from MSH segment"));
430+
Assert.True(exception?.Message.Contains("Can't parse critical fields from MSH segment"));
431+
}
432+
433+
[Test]
434+
public void TestUnexpectedSegmentHintsDefault()
435+
{
436+
var message =
437+
"MSH|^~\\&|DATASERVICES|CORPORATE|||20120711120510.2-0500||ADT^A01^ADT_A01|9c906177-dfca-4bbe-9abd-d8eb43df93a0|D|2.6\r"
438+
+ "EVN||20120701000000-0500\r"
439+
+ "PID|1||397979797^^^SN^SN~4242^^^BKDMDM^PI~1000^^^YARDI^PI||Williams^Rory^H^^^^A||19641028000000-0600|M||||||||||31592^^^YARDI^AN\r"
440+
+ "NK1|1|Pond^Amelia^Q^^^^A|SPO|1234 Main St^^Sussex^WI^53089|^PRS^CP^^^^^^^^^555-1212||N\r"
441+
+ "NK1|2|Smith^John^^^^^A~^The Doctor^^^^^A|FND|1234 S Water St^^New London^WI^54961||^WPN^PH^^^^^^^^^555-9999|C\r"
442+
+ "PV1|2|I||R\r"
443+
+ "GT1|1||Doe^John^A^^^^A||5678 Maple Ave^^Sussex^WI^53089|^PRS^PH^^^^^^^^^555-9999|||||OTH\r"
444+
+ "IN1|1|CAP1000|YYDN|ACME HealthCare||||GR0000001|||||||HMO|||||||||||||||||||||PCY-0000042\r"
445+
+ "IN1|2||||||||||||||Medicare|||||||||||||||||||||123-45-6789-A\r"
446+
+ "IN1|3||||||||||||||Medicaid|||||||||||||||||||||987654321L\r"
447+
+ "ZFA|6|31592|12345|YARDI|20120201000000-0600";
448+
449+
var parser = new PipeParser();
450+
451+
var msg = (NHapi.Model.V26.Message.ADT_A01)parser.Parse(message);
452+
453+
var zfas = msg.GetINSURANCE(2).GetAll("ZFA");
454+
455+
Assert.AreEqual(1, zfas.Length);
456+
}
457+
458+
/// <summary>
459+
/// the following 3 tests were ported from
460+
/// <see href="https://github.com/hapifhir/hapi-hl7v2/blob/3333e3aeae60afb7493f6570456e6280c0e16c0b/hapi-test/src/test/java/ca/uhn/hl7v2/parser/NewPipeParserTest.java#L313">hapi</see>.
461+
/// <para>
462+
/// The original feature request for hapi is <seealso href="http://sourceforge.net/p/hl7api/feature-requests/64/">here</seealso>.
463+
/// </para>
464+
/// </summary>
465+
[Test]
466+
public void TestUnexpectedSegmentHintsInline()
467+
{
468+
var message =
469+
"MSH|^~\\&|DATASERVICES|CORPORATE|||20120711120510.2-0500||ADT^A01^ADT_A01|9c906177-dfca-4bbe-9abd-d8eb43df93a0|D|2.6\r"
470+
+ "EVN||20120701000000-0500\r"
471+
+ "PID|1||397979797^^^SN^SN~4242^^^BKDMDM^PI~1000^^^YARDI^PI||Williams^Rory^H^^^^A||19641028000000-0600|M||||||||||31592^^^YARDI^AN\r"
472+
+ "NK1|1|Pond^Amelia^Q^^^^A|SPO|1234 Main St^^Sussex^WI^53089|^PRS^CP^^^^^^^^^555-1212||N\r"
473+
+ "NK1|2|Smith^John^^^^^A~^The Doctor^^^^^A|FND|1234 S Water St^^New London^WI^54961||^WPN^PH^^^^^^^^^555-9999|C\r"
474+
+ "PV1|2|I||R\r"
475+
+ "GT1|1||Doe^John^A^^^^A||5678 Maple Ave^^Sussex^WI^53089|^PRS^PH^^^^^^^^^555-9999|||||OTH\r"
476+
+ "IN1|1|CAP1000|YYDN|ACME HealthCare||||GR0000001|||||||HMO|||||||||||||||||||||PCY-0000042\r"
477+
+ "IN1|2||||||||||||||Medicare|||||||||||||||||||||123-45-6789-A\r"
478+
+ "IN1|3||||||||||||||Medicaid|||||||||||||||||||||987654321L\r"
479+
+ "ZFA|6|31592|12345|YARDI|20120201000000-0600";
480+
481+
var parser = new PipeParser();
482+
var options = new ParserOptions { UnexpectedSegmentBehaviour = UnexpectedSegmentBehaviour.AddInline };
483+
484+
var msg = (NHapi.Model.V26.Message.ADT_A01)parser.Parse(message, options);
485+
486+
var zfas = msg.GetINSURANCE(2).GetAll("ZFA");
487+
488+
Assert.AreEqual(1, zfas.Length);
489+
}
490+
491+
[Test]
492+
public void TestUnexpectedSegmentHintsDropToRoot()
493+
{
494+
var message =
495+
"MSH|^~\\&|DATASERVICES|CORPORATE|||20120711120510.2-0500||ADT^A01^ADT_A01|9c906177-dfca-4bbe-9abd-d8eb43df93a0|D|2.6\r"
496+
+ "ZZA|1\r"
497+
+ "EVN||20120701000000-0500\r"
498+
+ "PID|1||397979797^^^SN^SN~4242^^^BKDMDM^PI~1000^^^YARDI^PI||Williams^Rory^H^^^^A||19641028000000-0600|M||||||||||31592^^^YARDI^AN\r"
499+
+ "NK1|1|Pond^Amelia^Q^^^^A|SPO|1234 Main St^^Sussex^WI^53089|^PRS^CP^^^^^^^^^555-1212||N\r"
500+
+ "NK1|2|Smith^John^^^^^A~^The Doctor^^^^^A|FND|1234 S Water St^^New London^WI^54961||^WPN^PH^^^^^^^^^555-9999|C\r"
501+
+ "PV1|2|I||R\r"
502+
+ "GT1|1||Doe^John^A^^^^A||5678 Maple Ave^^Sussex^WI^53089|^PRS^PH^^^^^^^^^555-9999|||||OTH\r"
503+
+ "IN1|1|CAP1000|YYDN|ACME HealthCare||||GR0000001|||||||HMO|||||||||||||||||||||PCY-0000042\r"
504+
+ "IN1|2||||||||||||||Medicare|||||||||||||||||||||123-45-6789-A\r"
505+
+ "IN1|3||||||||||||||Medicaid|||||||||||||||||||||987654321L\r"
506+
+ "ZFA|6|31592|12345|YARDI|20120201000000-0600";
507+
508+
var parser = new PipeParser();
509+
var options = new ParserOptions { UnexpectedSegmentBehaviour = UnexpectedSegmentBehaviour.DropToRoot };
510+
511+
var msg = (NHapi.Model.V26.Message.ADT_A01)parser.Parse(message, options);
512+
513+
var zzas = msg.GetAll("ZZA");
514+
var zfas = msg.GetAll("ZFA");
515+
516+
Assert.AreEqual(1, zfas.Length);
517+
Assert.AreEqual(1, zzas.Length);
518+
}
519+
520+
[Test]
521+
public void TestUnexpectedSegmentHintsThrowHl7Exception()
522+
{
523+
var message =
524+
"MSH|^~\\&|DATASERVICES|CORPORATE|||20120711120510.2-0500||ADT^A01^ADT_A01|9c906177-dfca-4bbe-9abd-d8eb43df93a0|D|2.6\r"
525+
+ "EVN||20120701000000-0500\r"
526+
+ "PID|1||397979797^^^SN^SN~4242^^^BKDMDM^PI~1000^^^YARDI^PI||Williams^Rory^H^^^^A||19641028000000-0600|M||||||||||31592^^^YARDI^AN\r"
527+
+ "NK1|1|Pond^Amelia^Q^^^^A|SPO|1234 Main St^^Sussex^WI^53089|^PRS^CP^^^^^^^^^555-1212||N\r"
528+
+ "NK1|2|Smith^John^^^^^A~^The Doctor^^^^^A|FND|1234 S Water St^^New London^WI^54961||^WPN^PH^^^^^^^^^555-9999|C\r"
529+
+ "PV1|2|I||R\r"
530+
+ "GT1|1||Doe^John^A^^^^A||5678 Maple Ave^^Sussex^WI^53089|^PRS^PH^^^^^^^^^555-9999|||||OTH\r"
531+
+ "IN1|1|CAP1000|YYDN|ACME HealthCare||||GR0000001|||||||HMO|||||||||||||||||||||PCY-0000042\r"
532+
+ "IN1|2||||||||||||||Medicare|||||||||||||||||||||123-45-6789-A\r"
533+
+ "IN1|3||||||||||||||Medicaid|||||||||||||||||||||987654321L\r"
534+
+ "ZFA|6|31592|12345|YARDI|20120201000000-0600";
535+
536+
var parser = new PipeParser();
537+
var options = new ParserOptions { UnexpectedSegmentBehaviour = UnexpectedSegmentBehaviour.ThrowHl7Exception };
538+
539+
var exception = Assert.Throws<HL7Exception>(() => parser.Parse(message, options));
540+
541+
Assert.AreEqual("Found unknown segment: ZFA", exception?.Message);
432542
}
433543
}
434544
}

0 commit comments

Comments
 (0)