Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor rs-e2e project to remove Hapi HL7 library #1654

Open
wants to merge 69 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
347c87c
WIP: refactored to remove hapi hl7 library
basiliskus Dec 12, 2024
8fd2209
Moved files to more appropiate location
basiliskus Dec 12, 2024
600cbd9
Update HL7MessageTest.groovy
luis-pabon-tf Dec 12, 2024
ad70a59
Merge branch 'story/1532/remove-hapi-hl7-lib' of https://github.com/C…
luis-pabon-tf Dec 12, 2024
d78067b
Accounting for edge cases
basiliskus Dec 12, 2024
95cfd1d
Made class with static methods static
basiliskus Dec 12, 2024
d0d73b2
Improved delimiter handling and added test assertions
basiliskus Dec 12, 2024
245f572
Handling additional edge case
basiliskus Dec 12, 2024
b78bbaf
Merge branch 'main' into story/1532/remove-hapi-hl7-lib
luis-pabon-tf Dec 12, 2024
d18a563
Update HL7MessageTest.groovy
luis-pabon-tf Dec 12, 2024
47a7030
Improved encoding character handling to use defaults and move to HL7P…
basiliskus Dec 12, 2024
d16b7e5
Update HapiHL7ExpressionEvaluatorTest.groovy
luis-pabon-tf Dec 12, 2024
d29df3a
Extracted parsing logic to HL7Parser
basiliskus Dec 13, 2024
69e45cb
Added method to return string of message and fixed a couple of tests
basiliskus Dec 13, 2024
97d9196
Moved flies from /external/hapi folder to /hl7 folder
basiliskus Dec 13, 2024
3968569
Renamed files to remove Hapi prefix
basiliskus Dec 13, 2024
1168d01
Renamed files to remove Hapi prefix
basiliskus Dec 13, 2024
e123db1
Replaced magic strings
basiliskus Dec 13, 2024
6db46de
Update HL7Message.java
luis-pabon-tf Dec 13, 2024
c3c1223
Update HL7ExpressionEvaluator.java
luis-pabon-tf Dec 13, 2024
6c704cf
Updating tests and some cleanup
luis-pabon-tf Dec 13, 2024
de65a59
Merge branch 'main' into story/1532/remove-hapi-hl7-lib
luis-pabon-tf Dec 13, 2024
527dc53
Update HL7ExpressionEvaluator.java
luis-pabon-tf Dec 13, 2024
0350cbd
Update HL7ExpressionEvaluatorTest.groovy
luis-pabon-tf Dec 13, 2024
a1381ef
Refactored for HL7ExpressionEvaluator to use the new parser and remov…
basiliskus Dec 13, 2024
362ca7a
Update HL7ExpressionEvaluatorTest.groovy
luis-pabon-tf Dec 13, 2024
07b2aa4
Merge branch 'main' into story/1532/remove-hapi-hl7-lib
luis-pabon-tf Dec 13, 2024
031b66c
Fixed test + cleanup
basiliskus Dec 13, 2024
b4380a6
Fixed a couple of issues in HL7Message and refactored to extract segm…
basiliskus Dec 16, 2024
acafb51
Fixed HL7MessageTest tests
basiliskus Dec 17, 2024
8371cc3
Added HL7MessageException, improved error handling in HL7Message, ref…
basiliskus Dec 17, 2024
4b278aa
Update HL7FileMatcherTest.groovy
luis-pabon-tf Dec 17, 2024
8666d1f
Merge branch 'main' into story/1532/remove-hapi-hl7-lib
luis-pabon-tf Dec 17, 2024
ea1d47f
Added test cobverage for HL7Message and fixed parsing issue
basiliskus Dec 17, 2024
336a6f1
Merge branch 'story/1532/remove-hapi-hl7-lib' of github.com:CDCgov/tr…
basiliskus Dec 17, 2024
f4290aa
Add more HL7Parser coverage
luis-pabon-tf Dec 17, 2024
bc6e64e
Added test coverage for HL7ParserTest
basiliskus Dec 17, 2024
b73037b
Removed unused HL7Segment.getIndex
basiliskus Dec 17, 2024
2a157a7
Completed HL7Parser coverage
luis-pabon-tf Dec 17, 2024
3f8acfe
Merge branch 'story/1532/remove-hapi-hl7-lib' of https://github.com/C…
luis-pabon-tf Dec 17, 2024
01bf3c1
Removed gradle dependency to the hapi library and removed last few me…
basiliskus Dec 17, 2024
a7c1532
Added test coverage and changed exception type
basiliskus Dec 18, 2024
aabbb4d
Fixed a couple of issues brought up by sonar
basiliskus Dec 18, 2024
cbe5508
Replacing IllegalArgumentException with HL7ParserException custom exc…
basiliskus Dec 18, 2024
3e8ae35
Changed parser behaviour to return an empty string instead of null wh…
basiliskus Dec 18, 2024
cbf9762
Added a not to the readme about returning an empty string when value …
basiliskus Dec 18, 2024
0d9b31d
Added javadocs
basiliskus Dec 18, 2024
733a48c
Merge branch 'main' into story/1532/remove-hapi-hl7-lib
luis-pabon-tf Dec 18, 2024
d92bca1
Merge branch 'main' into story/1532/remove-hapi-hl7-lib
luis-pabon-tf Dec 19, 2024
9b7f2e2
Merge branch 'main' into story/1532/remove-hapi-hl7-lib
luis-pabon-tf Dec 19, 2024
d34f1ba
Refactored for better separation of concerns. Created HL7Path and HL7…
basiliskus Dec 20, 2024
38556b6
Fixed tests, added test coverage and some more refactoring
basiliskus Dec 20, 2024
7770998
Fixed a few more tests. A couple more to go
basiliskus Dec 20, 2024
b64eca5
Added javadocs
basiliskus Dec 23, 2024
fe296f6
Updated edge case handling and fixed test
basiliskus Dec 23, 2024
eaadd34
Merge branch 'main' into story/1532/remove-hapi-hl7-lib
luis-pabon-tf Dec 23, 2024
e74266f
Fixed remaining test
basiliskus Dec 23, 2024
b845dcd
Removed unnecessary logic
basiliskus Dec 23, 2024
12ee956
Override equals method in HL7Path to address issue raised by SonarCloud
basiliskus Dec 23, 2024
c157395
Need to override hashCode and toString as well
basiliskus Dec 23, 2024
c524996
Added test coverage for new methods
basiliskus Dec 23, 2024
e350ce1
Moved logic in HL7Parser.parseMessageFieldValue to HL7Message.getValu…
basiliskus Dec 23, 2024
466c3f4
Moved logic in HL7Parser.parsePath to HL7Path.parse to avoid circular…
basiliskus Dec 23, 2024
4c0a923
Removed HL7Parser and moved message parsing logic to HL7Message to av…
basiliskus Dec 23, 2024
905fa8d
Fixed equals overload logic
basiliskus Dec 26, 2024
089f0e6
Merge branch 'main' into story/1532/remove-hapi-hl7-lib
basiliskus Dec 26, 2024
e2aad2e
Merge branch 'main' into story/1532/remove-hapi-hl7-lib
luis-pabon-tf Dec 27, 2024
a773d70
Update HL7MessageException.java
luis-pabon-tf Dec 27, 2024
b81125c
Update HL7ExpressionEvaluatorTest.groovy
luis-pabon-tf Dec 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package gov.hhs.cdc.trustedintermediary.rse2e;

import gov.hhs.cdc.trustedintermediary.wrappers.HealthData;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
* Represents a HAPI HL7 message that implements the HealthData interface. This class provides a
* wrapper around the HAPI Message object.
*/
public class HL7Message implements HealthData<HL7Message> {

private final Map<String, List<String>> segments;
private final char[] delimiters;

public HL7Message(Map<String, List<String>> segments, char[] delimiters) {
this.segments = segments;
this.delimiters = delimiters;
}

public List<String> getSegment(String segment) {
return segments.get(segment);
}

public String getValue(String segmentName, int... indices) {
List<String> fields = segments.get(segmentName);
if (fields == null || indices[0] > fields.size()) {
return null;
}

String value = fields.get(indices[0] - 1);
for (int i = 1; i < indices.length; i++) {
value = splitAndGet(value, delimiters[i - 1], indices[i]);
if (value == null) return null;
}
return value;
}

private String splitAndGet(String value, char delimiter, int index) {
String[] parts = value.split(Pattern.quote(String.valueOf(delimiter)));
return index <= parts.length ? parts[index - 1] : null;
}

@Override
public HL7Message getUnderlyingData() {
return this;
}

@Override
public String getIdentifier() {
return getValue("MSH", 10);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package gov.hhs.cdc.trustedintermediary.rse2e;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;

public class HL7Parser {

private static final HL7Parser INSTANCE = new HL7Parser();

private static final String NEWLINE_REGEX = "\\r?\\n|\\r";
private static final char DEFAULT_FIELD_SEPARATOR = '|';

private HL7Parser() {}

public static HL7Parser getInstance() {
return INSTANCE;
}

public static HL7Message parse(String content) {
Map<String, List<String>> segments = new HashMap<>();
char[] encodingCharacters = null;
String[] lines = content.split(NEWLINE_REGEX);
for (String line : lines) {
if (line.trim().isEmpty()) continue;
String[] fields = line.split(Pattern.quote(String.valueOf(DEFAULT_FIELD_SEPARATOR)));
String segmentName = fields[0];
List<String> segmentFields =
new ArrayList<>(Arrays.asList(fields).subList(1, fields.length));
if (Objects.equals(segmentName, "MSH")) {
encodingCharacters = fields[1].toCharArray();
segmentFields.add(0, String.valueOf(DEFAULT_FIELD_SEPARATOR));
}
segments.put(segmentName, segmentFields);
}
return new HL7Message(segments, encodingCharacters);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public final boolean evaluateExpression(String expression, HealthData<?>... data
String rightOperand =
matcher.group(3); // e.g. MSH-5.1, input.MSH-5.1, 'EPIC', ('EPIC', 'CERNER'), 2

// TODO: replace with our own Message implementation
Message outputMessage = (Message) data[0].getUnderlyingData();
Message inputMessage = (data.length > 1) ? (Message) data[1].getUnderlyingData() : null;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi;

import ca.uhn.hl7v2.DefaultHapiContext;
import ca.uhn.hl7v2.HL7Exception;
import ca.uhn.hl7v2.HapiContext;
import ca.uhn.hl7v2.model.Message;
import ca.uhn.hl7v2.parser.Parser;
import com.google.common.collect.Sets;
import gov.hhs.cdc.trustedintermediary.rse2e.HL7FileStream;
import gov.hhs.cdc.trustedintermediary.rse2e.HL7Message;
import gov.hhs.cdc.trustedintermediary.rse2e.HL7Parser;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
Expand All @@ -31,13 +28,13 @@ public static HapiHL7FileMatcher getInstance() {
return INSTANCE;
}

public Map<HapiHL7Message, HapiHL7Message> matchFiles(
public Map<HL7Message, HL7Message> matchFiles(
List<HL7FileStream> outputFiles, List<HL7FileStream> inputFiles)
throws HapiHL7FileMatcherException {
// We pair up output and input files based on the control ID, which is in MSH-10
// Any files (either input or output) that don't have a match are logged
Map<String, HapiHL7Message> inputMap = parseAndMapMessageByControlId(inputFiles);
Map<String, HapiHL7Message> outputMap = parseAndMapMessageByControlId(outputFiles);
Map<String, HL7Message> inputMap = parseAndMapMessageByControlId(inputFiles);
Map<String, HL7Message> outputMap = parseAndMapMessageByControlId(outputFiles);

Set<String> inputKeys = inputMap.keySet();
Set<String> outputKeys = outputMap.keySet();
Expand All @@ -54,37 +51,31 @@ public Map<HapiHL7Message, HapiHL7Message> matchFiles(
return inputKeys.stream().collect(Collectors.toMap(inputMap::get, outputMap::get));
}

public Map<String, HapiHL7Message> parseAndMapMessageByControlId(List<HL7FileStream> files)
public Map<String, HL7Message> parseAndMapMessageByControlId(List<HL7FileStream> files)
throws HapiHL7FileMatcherException {

Map<String, HapiHL7Message> messageMap = new HashMap<>();
Map<String, HL7Message> messageMap = new HashMap<>();

try (HapiContext context = new DefaultHapiContext()) {
Parser parser = context.getPipeParser();

for (HL7FileStream hl7FileStream : files) {
String fileName = hl7FileStream.fileName();
try (InputStream inputStream = hl7FileStream.inputStream()) {
String content = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
Message message = parser.parse(content);
HapiHL7Message hapiHL7Message = new HapiHL7Message(message);
String msh10 = hapiHL7Message.getIdentifier();
if (msh10 == null || msh10.isEmpty()) {
throw new HapiHL7FileMatcherException(
String.format("MSH-10 is empty for file: %s", fileName));
}
messageMap.put(msh10, hapiHL7Message);
} catch (HL7Exception e) {
throw new HapiHL7FileMatcherException(
String.format("Failed to parse HL7 message from file: %s", fileName),
e);
} catch (IOException e) {
for (HL7FileStream hl7FileStream : files) {
String fileName = hl7FileStream.fileName();
try (InputStream inputStream = hl7FileStream.inputStream()) {
String content = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
HL7Message message = HL7Parser.parse(content);
String msh10 = message.getIdentifier();
if (msh10 == null || msh10.isEmpty()) {
throw new HapiHL7FileMatcherException(
String.format("Failed to read file: %s", fileName), e);
String.format("MSH-10 is empty for file: %s", fileName));
}
messageMap.put(msh10, message);
// } catch (HL7Exception e) {
luis-pabon-tf marked this conversation as resolved.
Show resolved Hide resolved
// throw new HapiHL7FileMatcherException(
// String.format("Failed to parse HL7 message from file:
// %s", fileName),
// e);
} catch (IOException e) {
throw new HapiHL7FileMatcherException(
String.format("Failed to read file: %s", fileName), e);
}
} catch (IOException e) {
throw new HapiHL7FileMatcherException("Failed to close input stream", e);
}

return messageMap;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package gov.hhs.cdc.trustedintermediary.rse2e

import spock.lang.Specification

class HL7ParserTest extends Specification {

def "test defined assertions on relevant messages"() {
given:
def content = """MSH|^~\\&|Sender Application^sender.test.com^DNS|Sender Facility^0.0.0.0.0.0.0.0^ISO|Receiver Application^0.0.0.0.0.0.0.0^ISO|Receiver Facility^automated-staging-test-receiver-id^DNS|20230101010000-0000||ORM^O01^ORM_O01|001|N|2.5.1||||||||||
PID|1||1300974^^^Baptist East^MR||ONE^TESTCASE||202402210152-0500|F^Female^HL70001||2106-3^White^HL70005|1234 GPCS WAY^^MONTGOMERY^Alabama^36117^USA^home^^Montgomery|||||||2227600015||||N^Not Hispanic or Latino^HL70189|||1|||||
NK1|1|ONE^MOMFIRST|MTH^Mother^HL70063||^^^^^804^5693861||||||||||||||||||||||||||||123456789^^^Medicaid&2.16.840.1.113883.4.446&ISO^MD||||000-00-0000^^^ssn&2.16.840.1.113883.4.1&ISO^SS
ORC|NW|4560411583^ORDERID||||||||||12345^^^^^^^^NPI&2.16.840.1.113883.4.6&ISO^L|||||||||
OBR|1|4560411583^ORDERID||54089-8^Newborn screening panel AHIC^LN|||202402221854-0500|||||||||12345^^^^^^^^NPI&2.16.840.1.113883.4.6&ISO^L||||||||
OBX|1|ST|57723-9^Unique bar code number of Current sample^LN||123456||||||F|||202402221854-0500
"""

when:
HL7Message message = HL7Parser.parse(content)
String value = message.getValue("PID", 3, 4)

then:
value == "Baptist East"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi
import ca.uhn.hl7v2.model.Message
import ca.uhn.hl7v2.model.v251.message.ORU_R01
import ca.uhn.hl7v2.model.v251.segment.MSH
import gov.hhs.cdc.trustedintermediary.rse2e.HL7Message
import spock.lang.Specification

class HapiHL7MessageTest extends Specification {
class HL7MessageTest extends Specification {

def "getUnderlyingData should correctly initialize and return underlying data"() {
given:
def mockMessage = Mock(Message)
def hl7Message = new HapiHL7Message(mockMessage)
def hl7Message = new HL7Message(mockMessage)

expect:
hl7Message.getUnderlyingData() == mockMessage
Expand All @@ -24,7 +25,7 @@ class HapiHL7MessageTest extends Specification {
mshSegment.getMessageControlID().setValue(expectedIdentifier)

and:
def hl7Message = new HapiHL7Message(oruMessage)
def hl7Message = new HL7Message(oruMessage)

when:
def name = hl7Message.getIdentifier()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi

import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext
import gov.hhs.cdc.trustedintermediary.rse2e.HL7FileStream
import gov.hhs.cdc.trustedintermediary.rse2e.HL7Message
import gov.hhs.cdc.trustedintermediary.wrappers.Logger
import spock.lang.Specification

Expand Down Expand Up @@ -30,10 +31,10 @@ class HapiHL7FileMatcherTest extends Specification {
new HL7FileStream("outputFileStream1", Mock(InputStream)),
new HL7FileStream("outputFileStream2", Mock(InputStream))
]
def mockInputMessage1 = Mock(HapiHL7Message)
def mockInputMessage2 = Mock(HapiHL7Message)
def mockOutputMessage1 = Mock(HapiHL7Message)
def mockOutputMessage2 = Mock(HapiHL7Message)
def mockInputMessage1 = Mock(HL7Message)
def mockInputMessage2 = Mock(HL7Message)
def mockOutputMessage1 = Mock(HL7Message)
def mockOutputMessage2 = Mock(HL7Message)
spyFileMatcher.parseAndMapMessageByControlId(mockInputFiles) >> ["001": mockInputMessage1, "002": mockInputMessage2 ]
spyFileMatcher.parseAndMapMessageByControlId(mockOutputFiles) >> ["001": mockOutputMessage1, "002": mockOutputMessage2 ]

Expand All @@ -60,8 +61,8 @@ class HapiHL7FileMatcherTest extends Specification {
mockOutputFiles = [
new HL7FileStream("matchingOutputFileStream", Mock(InputStream))
]
spyFileMatcher.parseAndMapMessageByControlId(mockInputFiles) >> ["001": Mock(HapiHL7Message), "002": Mock(HapiHL7Message) ]
spyFileMatcher.parseAndMapMessageByControlId(mockOutputFiles) >> ["001": Mock(HapiHL7Message) ]
spyFileMatcher.parseAndMapMessageByControlId(mockInputFiles) >> ["001": Mock(HL7Message), "002": Mock(HL7Message) ]
spyFileMatcher.parseAndMapMessageByControlId(mockOutputFiles) >> ["001": Mock(HL7Message) ]
spyFileMatcher.matchFiles(mockOutputFiles, mockInputFiles)

then:
Expand All @@ -79,8 +80,8 @@ class HapiHL7FileMatcherTest extends Specification {
new HL7FileStream("matchingOutputFileStream", Mock(InputStream)),
new HL7FileStream("nonMatchingOutputFileStream", Mock(InputStream))
]
spyFileMatcher.parseAndMapMessageByControlId(mockInputFiles) >> ["001": Mock(HapiHL7Message) ]
spyFileMatcher.parseAndMapMessageByControlId(mockOutputFiles) >> ["001": Mock(HapiHL7Message), "003": Mock(HapiHL7Message) ]
spyFileMatcher.parseAndMapMessageByControlId(mockInputFiles) >> ["001": Mock(HL7Message) ]
spyFileMatcher.parseAndMapMessageByControlId(mockOutputFiles) >> ["001": Mock(HL7Message), "003": Mock(HL7Message) ]
spyFileMatcher.matchFiles(mockOutputFiles, mockInputFiles)

then:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package gov.hhs.cdc.trustedintermediary.rse2e.ruleengine

import ca.uhn.hl7v2.model.Message
import gov.hhs.cdc.trustedintermediary.rse2e.external.hapi.HapiHL7Message

import gov.hhs.cdc.trustedintermediary.rse2e.HL7Message
import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoader
import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoaderException
import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext
Expand Down Expand Up @@ -85,7 +85,7 @@ class AssertionRuleEngineTest extends Specification {
}

when:
ruleEngine.runRules(Mock(HapiHL7Message), Mock(HapiHL7Message))
ruleEngine.runRules(Mock(HL7Message), Mock(HL7Message))

then:
1 * mockLogger.logError(_ as String, exception)
Expand All @@ -97,15 +97,15 @@ class AssertionRuleEngineTest extends Specification {
mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) >> { throw exception }

when:
ruleEngine.runRules(Mock(HapiHL7Message), Mock(HapiHL7Message))
ruleEngine.runRules(Mock(HL7Message), Mock(HL7Message))

then:
1 * mockLogger.logError(_ as String, exception)
}

def "runRules returns nothing when there are no rules"() {
when:
def result = ruleEngine.runRules(Mock(HapiHL7Message), Mock(HapiHL7Message))
def result = ruleEngine.runRules(Mock(HL7Message), Mock(HL7Message))

then:
result.isEmpty()
Expand All @@ -117,7 +117,7 @@ class AssertionRuleEngineTest extends Specification {
mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) >> [rule]

when:
def result = ruleEngine.runRules(Mock(HapiHL7Message), Mock(HapiHL7Message))
def result = ruleEngine.runRules(Mock(HL7Message), Mock(HL7Message))

then:
result.size() == 1
Expand All @@ -130,7 +130,7 @@ class AssertionRuleEngineTest extends Specification {
mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) >> [rule]

when:
def result = ruleEngine.runRules(Mock(HapiHL7Message), Mock(HapiHL7Message))
def result = ruleEngine.runRules(Mock(HL7Message), Mock(HL7Message))

then:
result.isEmpty()
Expand Down
Loading