diff --git a/nifi-nar-bundles/nifi-hl7-bundle/nifi-hl7-processors/src/main/java/org/apache/nifi/processors/hl7/ExtractHL7Attributes.java b/nifi-nar-bundles/nifi-hl7-bundle/nifi-hl7-processors/src/main/java/org/apache/nifi/processors/hl7/ExtractHL7Attributes.java index 6c35637534..ac5b18e0bd 100644 --- a/nifi-nar-bundles/nifi-hl7-bundle/nifi-hl7-processors/src/main/java/org/apache/nifi/processors/hl7/ExtractHL7Attributes.java +++ b/nifi-nar-bundles/nifi-hl7-bundle/nifi-hl7-processors/src/main/java/org/apache/nifi/processors/hl7/ExtractHL7Attributes.java @@ -20,11 +20,14 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.text.WordUtils; import org.apache.nifi.annotation.behavior.InputRequirement; import org.apache.nifi.annotation.behavior.InputRequirement.Requirement; @@ -46,11 +49,14 @@ import org.apache.nifi.stream.io.StreamUtils; import ca.uhn.hl7v2.DefaultHapiContext; import ca.uhn.hl7v2.HL7Exception; import ca.uhn.hl7v2.HapiContext; +import ca.uhn.hl7v2.model.Composite; import ca.uhn.hl7v2.model.Group; import ca.uhn.hl7v2.model.Message; import ca.uhn.hl7v2.model.Segment; import ca.uhn.hl7v2.model.Structure; import ca.uhn.hl7v2.model.Type; +import ca.uhn.hl7v2.model.Visitable; +import ca.uhn.hl7v2.parser.CanonicalModelClassFactory; import ca.uhn.hl7v2.parser.DefaultEscaping; import ca.uhn.hl7v2.parser.EncodingCharacters; import ca.uhn.hl7v2.parser.Escaping; @@ -68,8 +74,13 @@ import ca.uhn.hl7v2.validation.impl.ValidationContextFactory; + "a value of \"2.1\" and an attribute named \"OBX_11.3\" with a value of \"93000^CPT4\".") public class ExtractHL7Attributes extends AbstractProcessor { + private static final EncodingCharacters HL7_ENCODING = EncodingCharacters.defaultInstance(); + + private static final Escaping HL7_ESCAPING = new DefaultEscaping(); + public static final PropertyDescriptor CHARACTER_SET = new PropertyDescriptor.Builder() .name("Character Encoding") + .displayName("Character Encoding") .description("The Character Encoding that is used to encode the HL7 data") .required(true) .expressionLanguageSupported(true) @@ -77,6 +88,46 @@ public class ExtractHL7Attributes extends AbstractProcessor { .defaultValue("UTF-8") .build(); + public static final PropertyDescriptor USE_SEGMENT_NAMES = new PropertyDescriptor.Builder() + .name("use-segment-names") + .displayName("Use Segment Names") + .description("Whether or not to use HL7 segment names in attributes") + .required(true) + .allowableValues("true", "false") + .defaultValue("false") + .addValidator(StandardValidators.BOOLEAN_VALIDATOR) + .build(); + + public static final PropertyDescriptor PARSE_SEGMENT_FIELDS = new PropertyDescriptor.Builder() + .name("parse-segment-fields") + .displayName("Parse Segment Fields") + .description("Whether or not to parse HL7 segment fields into attributes") + .required(true) + .allowableValues("true", "false") + .defaultValue("false") + .addValidator(StandardValidators.BOOLEAN_VALIDATOR) + .build(); + + public static final PropertyDescriptor SKIP_VALIDATION = new PropertyDescriptor.Builder() + .name("skip-validation") + .displayName("Skip Validation") + .description("Whether or not to validate HL7 message values") + .required(true) + .allowableValues("true", "false") + .defaultValue("true") + .addValidator(StandardValidators.BOOLEAN_VALIDATOR) + .build(); + + public static final PropertyDescriptor HL7_INPUT_VERSION = new PropertyDescriptor.Builder() + .name("hl7-input-version") + .displayName("HL7 Input Version") + .description("The HL7 version to use for parsing and validation") + .required(true) + .allowableValues("autodetect", "2.2", "2.3", "2.3.1", "2.4", "2.5", "2.5.1", "2.6") + .defaultValue("autodetect") + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + public static final Relationship REL_SUCCESS = new Relationship.Builder() .name("success") .description("A FlowFile is routed to this relationship if it is properly parsed as HL7 and its attributes extracted") @@ -91,6 +142,10 @@ public class ExtractHL7Attributes extends AbstractProcessor { protected List getSupportedPropertyDescriptors() { final List properties = new ArrayList<>(); properties.add(CHARACTER_SET); + properties.add(USE_SEGMENT_NAMES); + properties.add(PARSE_SEGMENT_FIELDS); + properties.add(SKIP_VALIDATION); + properties.add(HL7_INPUT_VERSION); return properties; } @@ -110,6 +165,10 @@ public class ExtractHL7Attributes extends AbstractProcessor { } final Charset charset = Charset.forName(context.getProperty(CHARACTER_SET).evaluateAttributeExpressions(flowFile).getValue()); + final Boolean useSegmentNames = context.getProperty(USE_SEGMENT_NAMES).asBoolean(); + final Boolean parseSegmentFields = context.getProperty(PARSE_SEGMENT_FIELDS).asBoolean(); + final Boolean skipValidation = context.getProperty(SKIP_VALIDATION).asBoolean(); + final String inputVersion = context.getProperty(HL7_INPUT_VERSION).getValue(); final byte[] buffer = new byte[(int) flowFile.getSize()]; session.read(flowFile, new InputStreamCallback() { @@ -121,89 +180,172 @@ public class ExtractHL7Attributes extends AbstractProcessor { @SuppressWarnings("resource") final HapiContext hapiContext = new DefaultHapiContext(); - hapiContext.setValidationContext((ValidationContext) ValidationContextFactory.noValidation()); + if (!inputVersion.equals("autodetect")) { + hapiContext.setModelClassFactory(new CanonicalModelClassFactory(inputVersion)); + } + if (skipValidation) { + hapiContext.setValidationContext((ValidationContext) ValidationContextFactory.noValidation()); + } final PipeParser parser = hapiContext.getPipeParser(); final String hl7Text = new String(buffer, charset); - final Message message; try { - message = parser.parse(hl7Text); - final Group group = message.getParent(); - - final Map attributes = new HashMap<>(); - extractAttributes(group, attributes); + final Message message = parser.parse(hl7Text); + final Map attributes = getAttributes(message, useSegmentNames, parseSegmentFields); flowFile = session.putAllAttributes(flowFile, attributes); - getLogger().info("Successfully extracted {} attributes for {}; routing to success", new Object[]{attributes.size(), flowFile}); getLogger().debug("Added the following attributes for {}: {}", new Object[]{flowFile, attributes}); - session.transfer(flowFile, REL_SUCCESS); } catch (final HL7Exception e) { getLogger().error("Failed to extract attributes from {} due to {}", new Object[]{flowFile, e}); session.transfer(flowFile, REL_FAILURE); return; } + + session.transfer(flowFile, REL_SUCCESS); } - private void extractAttributes(final Group group, final Map attributes) throws HL7Exception { - extractAttributes(group, attributes, new HashMap()); - } - - private void extractAttributes(final Group group, final Map attributes, final Map segmentCounts) throws HL7Exception { - if (group.isEmpty()) { - return; - } - - final String[] structureNames = group.getNames(); - for (final String structName : structureNames) { - final Structure[] subStructures = group.getAll(structName); - - if (group.isGroup(structName)) { - for (final Structure subStructure : subStructures) { - final Group subGroup = (Group) subStructure; - extractAttributes(subGroup, attributes, segmentCounts); - } - } else { - for (final Structure structure : subStructures) { - final Segment segment = (Segment) structure; - - final String segmentName = segment.getName(); - Integer segmentNum = segmentCounts.get(segmentName); - if (segmentNum == null) { - segmentNum = 1; - segmentCounts.put(segmentName, 1); + public static Map getAttributes(final Group group, final boolean useNames, final boolean parseFields) throws HL7Exception { + final Map attributes = new TreeMap<>(); + if (!isEmpty(group)) { + for (final Map.Entry segmentEntry : getAllSegments(group).entrySet()) { + final String segmentKey = segmentEntry.getKey(); + final Segment segment = segmentEntry.getValue(); + final Map fields = getAllFields(segmentKey, segment, useNames); + for (final Map.Entry fieldEntry : fields.entrySet()) { + final String fieldKey = fieldEntry.getKey(); + final Type field = fieldEntry.getValue(); + // These maybe should used the escaped values, but that would + // change the existing non-broken behavior of the processor + if (parseFields && (field instanceof Composite) && !isTimestamp(field)) { + for (final Map.Entry componentEntry : getAllComponents(fieldKey, field).entrySet()) { + final String componentKey = componentEntry.getKey(); + final Type component = componentEntry.getValue(); + final String componentValue = HL7_ESCAPING.unescape(component.encode(), HL7_ENCODING); + if (!StringUtils.isEmpty(componentValue)) { + attributes.put(componentKey, componentValue); + } + } } else { - segmentNum++; - segmentCounts.put(segmentName, segmentNum); + final String fieldValue = HL7_ESCAPING.unescape(field.encode(), HL7_ENCODING); + if (!StringUtils.isEmpty(fieldValue)) { + attributes.put(fieldKey, fieldValue); + } } - final boolean segmentRepeating = segment.getParent().isRepeating(segment.getName()); - final boolean parentRepeating = (segment.getParent().getParent() != segment.getParent() && segment.getParent().getParent().isRepeating(segment.getParent().getName())); - final boolean useSegmentIndex = segmentRepeating || parentRepeating; - - final Map attributeMap = getAttributes(segment, useSegmentIndex ? segmentNum : null); - attributes.putAll(attributeMap); } } } - } - - private Map getAttributes(final Segment segment, final Integer segmentNum) throws HL7Exception { - final Map attributes = new HashMap<>(); - - final EncodingCharacters encoding = EncodingCharacters.defaultInstance(); - final Escaping escaping = new DefaultEscaping(); - for (int i = 1; i <= segment.numFields(); i++) { - final String fieldName = segment.getName() + (segmentNum == null ? "" : "_" + segmentNum) + "." + i; - final Type[] types = segment.getField(i); - for (final Type type : types) { - // This maybe should used the escaped values, but that would - // change the existing non-broken behavior of the processor - final String escapedTypeValue = type.encode(); - final String unescapedTypeValue = escaping.unescape(escapedTypeValue, encoding); - attributes.put(fieldName, unescapedTypeValue); - } - } - return attributes; } + private static Map getAllSegments(final Group group) throws HL7Exception { + final Map segments = new TreeMap<>(); + addSegments(group, segments); + return segments; + } + + private static void addSegments(final Group group, final Map segments) throws HL7Exception { + if (!isEmpty(group)) { + for (final String name : group.getNames()) { + for (final Structure structure : group.getAll(name)) { + if (group.isGroup(name) && structure instanceof Group) { + addSegments((Group) structure, segments); + } else if (structure instanceof Segment) { + addSegments((Segment) structure, segments); + } + } + } + } + } + + private static void addSegments(final Segment segment, final Map segments) throws HL7Exception { + if (!isEmpty(segment)) { + final StringBuilder sb = new StringBuilder().append(segment.getName()); + if (isRepeating(segment)) { + final Type field = segment.getField(1, 0); + if (!isEmpty(field)) { + final String fieldValue = field.encode(); + final int segmentIndex = StringUtils.isEmpty(fieldValue) ? 1 : Integer.parseInt(fieldValue); + sb.append("_").append(segmentIndex); + } + } + final String segmentKey = sb.toString(); + segments.put(segmentKey, segment); + } + } + + private static Map getAllFields(final String segmentKey, final Segment segment, final boolean useNames) throws HL7Exception { + final Map fields = new TreeMap<>(); + final String[] segmentNames = segment.getNames(); + for (int i = 1; i <= segment.numFields(); i++) { + final Type field = segment.getField(i, 0); + if (!isEmpty(field)) { + final String fieldName; + if (useNames) { + fieldName = WordUtils.capitalize(segmentNames[i-1]).replaceAll("\\W+", ""); + } else { + fieldName = String.valueOf(i); + } + + final String fieldKey = new StringBuilder() + .append(segmentKey) + .append(".") + .append(fieldName) + .toString(); + + fields.put(fieldKey, field); + } + } + return fields; + } + + private static Map getAllComponents(final String fieldKey, final Type field) throws HL7Exception { + final Map components = new TreeMap<>(); + if (!isEmpty(field) && (field instanceof Composite)) { + final Type[] types = ((Composite) field).getComponents(); + for (int i = 0; i < types.length; i++) { + final Type type = types[i]; + if (!isEmpty(type)) { + String fieldName = field.getName(); + if (fieldName.equals("CM_MSG")) { + fieldName = "CM"; + } + final String typeKey = new StringBuilder() + .append(fieldKey) + .append(".") + .append(fieldName) + .append(".") + .append(i+1) + .toString(); + components.put(typeKey, type); + } + } + } + return components; + } + + private static boolean isTimestamp(final Type field) throws HL7Exception { + if (isEmpty(field)) { + return false; + } + final String fieldName = field.getName(); + return (fieldName.equals("TS") || fieldName.equals("DT") || fieldName.equals("TM")); + } + + private static boolean isRepeating(final Segment segment) throws HL7Exception { + if (isEmpty(segment)) { + return false; + } + + final Group parent = segment.getParent(); + final Group grandparent = parent.getParent(); + if (parent == grandparent) { + return false; + } + + return grandparent.isRepeating(parent.getName()); + } + + private static boolean isEmpty(final Visitable visitable) throws HL7Exception { + return (visitable == null || visitable.isEmpty()); + } } diff --git a/nifi-nar-bundles/nifi-hl7-bundle/nifi-hl7-processors/src/test/java/org/apache/nifi/processors/hl7/TestExtractHL7Attributes.java b/nifi-nar-bundles/nifi-hl7-bundle/nifi-hl7-processors/src/test/java/org/apache/nifi/processors/hl7/TestExtractHL7Attributes.java index e8ecc1ba64..cec8551a69 100644 --- a/nifi-nar-bundles/nifi-hl7-bundle/nifi-hl7-processors/src/test/java/org/apache/nifi/processors/hl7/TestExtractHL7Attributes.java +++ b/nifi-nar-bundles/nifi-hl7-bundle/nifi-hl7-processors/src/test/java/org/apache/nifi/processors/hl7/TestExtractHL7Attributes.java @@ -31,6 +31,13 @@ import java.util.TreeMap; public class TestExtractHL7Attributes { + public static final String TEST_INPUT_RECORD = + "MSH|^~\\&|XXXXXXXX||HealthProvider||||ORU^R01|Q1111111111111111111|P|2.3|\r\n" + + "PID|||12345^^^XYZ^MR||SMITH^JOHN||19700100|M||||||||||111111111111|123456789|\r\n" + + "PD1||||1234567890^LAST^FIRST^M^^^^^NPI|\r\n" + + "OBR|1|341856649^HNAM_ORDERID|000000000000000000|648088^Basic Metabolic Panel|||20150101000000|||||||||1620^Johnson^Corey^A||||||20150101000000|||F|||||||||||20150101000000|\r\n" + + "OBX|1|NM|GLU^Glucose Lvl|59|mg/dL|65-99^65^99|L|||F|||20150102000000|\r\n"; + @BeforeClass public static void setup() { System.setProperty("org.slf4j.simpleLogger.log.org.apache.nifi", "DEBUG"); @@ -38,13 +45,6 @@ public class TestExtractHL7Attributes { @Test public void testExtract() throws IOException { - final String inputRecord = - "MSH|^~\\&|XXXXXXXX||HealthProvider||||ORU^R01|Q1111111111111111111|P|2.3|\r\n" + - "PID|||12345^^^XYZ^MR||SMITH^JOHN||19700100|M||||||||||111111111111|123456789|\r\n" + - "PD1||||1234567890^LAST^FIRST^M^^^^^NPI|\r\n" + - "OBR|1|341856649^HNAM_ORDERID|000000000000000000|648088^Basic Metabolic Panel|||20150101000000|||||||||1620^Johnson^Corey^A||||||20150101000000|||F|||||||||||20150101000000|\r\n" + - "OBX|1|NM|GLU^Glucose Lvl|59|mg/dL|65-99^65^99|L|||F|||20150102000000|\r\n"; - final SortedMap expectedAttributes = new TreeMap<>(); // MSH.1 and MSH.2 could be escaped, but it's not clear which is right expectedAttributes.put("MSH.1", "|"); @@ -82,7 +82,7 @@ public class TestExtractHL7Attributes { expectedAttributes.put("PID.19", "123456789"); final TestRunner runner = TestRunners.newTestRunner(ExtractHL7Attributes.class); - runner.enqueue(inputRecord.getBytes(StandardCharsets.UTF_8)); + runner.enqueue(TEST_INPUT_RECORD.getBytes(StandardCharsets.UTF_8)); runner.run(); runner.assertAllFlowFilesTransferred(ExtractHL7Attributes.REL_SUCCESS, 1); @@ -100,6 +100,7 @@ public class TestExtractHL7Attributes { int mshSegmentCount = 0; int obrSegmentCount = 0; int obxSegmentCount = 0; + int pd1SegmentCount = 0; int pidSegmentCount = 0; for (final Map.Entry entry : sortedAttrs.entrySet()) { @@ -113,6 +114,9 @@ public class TestExtractHL7Attributes { } else if (entryKey.startsWith("OBX")) { obxSegmentCount++; continue; + } else if (entryKey.startsWith("PD1")) { + pd1SegmentCount++; + continue; } else if (entryKey.startsWith("PID")) { pidSegmentCount++; continue; @@ -122,6 +126,199 @@ public class TestExtractHL7Attributes { Assert.assertEquals("Did not have the proper number of MSH segments", 8, mshSegmentCount); Assert.assertEquals("Did not have the proper number of OBR segments", 9, obrSegmentCount); Assert.assertEquals("Did not have the proper number of OBX segments", 9, obxSegmentCount); + Assert.assertEquals("Did not have the proper number of PD1 segments", 1, pd1SegmentCount); Assert.assertEquals("Did not have the proper number of PID segments", 6, pidSegmentCount); } + + @Test + public void testExtractWithSegmentNames() throws IOException { + final SortedMap expectedAttributes = new TreeMap<>(); + expectedAttributes.put("MSH.FieldSeparator", "|"); + expectedAttributes.put("MSH.EncodingCharacters", "^~\\&"); + expectedAttributes.put("MSH.SendingApplication", "XXXXXXXX"); + expectedAttributes.put("MSH.ReceivingApplication", "HealthProvider"); + expectedAttributes.put("MSH.MessageType", "ORU^R01"); + expectedAttributes.put("MSH.MessageControlID", "Q1111111111111111111"); + expectedAttributes.put("MSH.ProcessingID", "P"); + expectedAttributes.put("MSH.VersionID", "2.3"); + expectedAttributes.put("OBR_1.SetIDObservationRequest", "1"); + expectedAttributes.put("OBR_1.PlacerOrderNumber", "341856649^HNAM_ORDERID"); + expectedAttributes.put("OBR_1.FillerOrderNumber", "000000000000000000"); + expectedAttributes.put("OBR_1.UniversalServiceIdentifier", "648088^Basic Metabolic Panel"); + expectedAttributes.put("OBR_1.ObservationDateTime", "20150101000000"); + expectedAttributes.put("OBR_1.OrderingProvider", "1620^Johnson^Corey^A"); + expectedAttributes.put("OBR_1.ResultsRptStatusChngDateTime", "20150101000000"); + expectedAttributes.put("OBR_1.ResultStatus", "F"); + expectedAttributes.put("OBR_1.ScheduledDateTime", "20150101000000"); + expectedAttributes.put("OBX_1.SetIDOBX", "1"); + expectedAttributes.put("OBX_1.ValueType", "NM"); + expectedAttributes.put("OBX_1.ObservationIdentifier", "GLU^Glucose Lvl"); + expectedAttributes.put("OBX_1.ObservationSubID", "59"); + expectedAttributes.put("OBX_1.ObservationValue", "mg/dL"); + expectedAttributes.put("OBX_1.Units", "65-99^65^99"); + expectedAttributes.put("OBX_1.ReferencesRange", "L"); + expectedAttributes.put("OBX_1.NatureOfAbnormalTest", "F"); + expectedAttributes.put("OBX_1.UserDefinedAccessChecks", "20150102000000"); + expectedAttributes.put("PD1.PatientPrimaryCareProviderNameIDNo", "1234567890^LAST^FIRST^M^^^^^NPI"); + expectedAttributes.put("PID.PatientIDInternalID", "12345^^^XYZ^MR"); + expectedAttributes.put("PID.PatientName", "SMITH^JOHN"); + expectedAttributes.put("PID.DateOfBirth", "19700100"); + expectedAttributes.put("PID.Sex", "M"); + expectedAttributes.put("PID.PatientAccountNumber", "111111111111"); + expectedAttributes.put("PID.SSNNumberPatient", "123456789"); + + final TestRunner runner = TestRunners.newTestRunner(ExtractHL7Attributes.class); + runner.setProperty(ExtractHL7Attributes.USE_SEGMENT_NAMES, "true"); + runner.enqueue(TEST_INPUT_RECORD.getBytes(StandardCharsets.UTF_8)); + + runner.run(); + runner.assertAllFlowFilesTransferred(ExtractHL7Attributes.REL_SUCCESS, 1); + + final MockFlowFile out = runner.getFlowFilesForRelationship(ExtractHL7Attributes.REL_SUCCESS).get(0); + final SortedMap sortedAttrs = new TreeMap<>(out.getAttributes()); + + for (final Map.Entry entry : expectedAttributes.entrySet()) { + final String key = entry.getKey(); + final String expected = entry.getValue(); + final String actual = sortedAttrs.get(key); + Assert.assertEquals(key + " segment values do not match", expected, actual); + } + + int mshSegmentCount = 0; + int obrSegmentCount = 0; + int obxSegmentCount = 0; + int pd1SegmentCount = 0; + int pidSegmentCount = 0; + + for (final Map.Entry entry : sortedAttrs.entrySet()) { + final String entryKey = entry.getKey(); + if (entryKey.startsWith("MSH")) { + mshSegmentCount++; + continue; + } else if (entryKey.startsWith("OBR")) { + obrSegmentCount++; + continue; + } else if (entryKey.startsWith("OBX")) { + obxSegmentCount++; + continue; + } else if (entryKey.startsWith("PD1")) { + pd1SegmentCount++; + continue; + } else if (entryKey.startsWith("PID")) { + pidSegmentCount++; + continue; + } + } + + Assert.assertEquals("Did not have the proper number of MSH segments", 8, mshSegmentCount); + Assert.assertEquals("Did not have the proper number of OBR segments", 9, obrSegmentCount); + Assert.assertEquals("Did not have the proper number of OBX segments", 9, obxSegmentCount); + Assert.assertEquals("Did not have the proper number of PD1 segments", 1, pd1SegmentCount); + Assert.assertEquals("Did not have the proper number of PID segments", 6, pidSegmentCount); + } + + @Test + public void testExtractWithSegmentNamesAndFields() throws IOException { + final SortedMap expectedAttributes = new TreeMap<>(); + expectedAttributes.put("MSH.FieldSeparator", "|"); + expectedAttributes.put("MSH.EncodingCharacters", "^~\\&"); + expectedAttributes.put("MSH.SendingApplication.HD.1", "XXXXXXXX"); + expectedAttributes.put("MSH.ReceivingApplication.HD.1", "HealthProvider"); + expectedAttributes.put("MSH.MessageType.CM.1", "ORU"); + expectedAttributes.put("MSH.MessageType.CM.2", "R01"); + expectedAttributes.put("MSH.MessageControlID", "Q1111111111111111111"); + expectedAttributes.put("MSH.ProcessingID.PT.1", "P"); + expectedAttributes.put("MSH.VersionID", "2.3"); + expectedAttributes.put("OBR_1.SetIDObservationRequest", "1"); + expectedAttributes.put("OBR_1.PlacerOrderNumber.EI.1", "341856649"); + expectedAttributes.put("OBR_1.PlacerOrderNumber.EI.2", "HNAM_ORDERID"); + expectedAttributes.put("OBR_1.FillerOrderNumber.EI.1", "000000000000000000"); + expectedAttributes.put("OBR_1.UniversalServiceIdentifier.CE.1", "648088"); + expectedAttributes.put("OBR_1.UniversalServiceIdentifier.CE.2", "Basic Metabolic Panel"); + expectedAttributes.put("OBR_1.ObservationDateTime", "20150101000000"); + expectedAttributes.put("OBR_1.OrderingProvider.XCN.1", "1620"); + expectedAttributes.put("OBR_1.OrderingProvider.XCN.2", "Johnson"); + expectedAttributes.put("OBR_1.OrderingProvider.XCN.3", "Corey"); + expectedAttributes.put("OBR_1.OrderingProvider.XCN.4", "A"); + expectedAttributes.put("OBR_1.ResultsRptStatusChngDateTime", "20150101000000"); + expectedAttributes.put("OBR_1.ResultStatus", "F"); + expectedAttributes.put("OBR_1.ScheduledDateTime", "20150101000000"); + expectedAttributes.put("OBX_1.SetIDOBX", "1"); + expectedAttributes.put("OBX_1.ValueType", "NM"); + expectedAttributes.put("OBX_1.ObservationIdentifier.CE.1", "GLU"); + expectedAttributes.put("OBX_1.ObservationIdentifier.CE.2", "Glucose Lvl"); + expectedAttributes.put("OBX_1.ObservationSubID", "59"); + expectedAttributes.put("OBX_1.ObservationValue", "mg/dL"); + expectedAttributes.put("OBX_1.Units.CE.1", "65-99"); + expectedAttributes.put("OBX_1.Units.CE.3", "65"); + expectedAttributes.put("OBX_1.Units.CE.3", "99"); + expectedAttributes.put("OBX_1.ReferencesRange", "L"); + expectedAttributes.put("OBX_1.NatureOfAbnormalTest", "F"); + expectedAttributes.put("OBX_1.UserDefinedAccessChecks", "20150102000000"); + expectedAttributes.put("PD1.PatientPrimaryCareProviderNameIDNo.XCN.1", "1234567890"); + expectedAttributes.put("PD1.PatientPrimaryCareProviderNameIDNo.XCN.2", "LAST"); + expectedAttributes.put("PD1.PatientPrimaryCareProviderNameIDNo.XCN.3", "FIRST"); + expectedAttributes.put("PD1.PatientPrimaryCareProviderNameIDNo.XCN.4", "M"); + expectedAttributes.put("PD1.PatientPrimaryCareProviderNameIDNo.XCN.9", "NPI"); + expectedAttributes.put("PID.PatientIDInternalID.CX.1", "12345"); + expectedAttributes.put("PID.PatientIDInternalID.CX.4", "XYZ"); + expectedAttributes.put("PID.PatientIDInternalID.CX.5", "MR"); + expectedAttributes.put("PID.PatientName.XPN.1", "SMITH"); + expectedAttributes.put("PID.PatientName.XPN.2", "JOHN"); + expectedAttributes.put("PID.DateOfBirth", "19700100"); + expectedAttributes.put("PID.Sex", "M"); + expectedAttributes.put("PID.PatientAccountNumber.CX.1", "111111111111"); + expectedAttributes.put("PID.SSNNumberPatient", "123456789"); + + final TestRunner runner = TestRunners.newTestRunner(ExtractHL7Attributes.class); + runner.setProperty(ExtractHL7Attributes.USE_SEGMENT_NAMES, "true"); + runner.setProperty(ExtractHL7Attributes.PARSE_SEGMENT_FIELDS, "true"); + runner.enqueue(TEST_INPUT_RECORD.getBytes(StandardCharsets.UTF_8)); + + runner.run(); + runner.assertAllFlowFilesTransferred(ExtractHL7Attributes.REL_SUCCESS, 1); + + final MockFlowFile out = runner.getFlowFilesForRelationship(ExtractHL7Attributes.REL_SUCCESS).get(0); + final SortedMap sortedAttrs = new TreeMap<>(out.getAttributes()); + + for (final Map.Entry entry : expectedAttributes.entrySet()) { + final String key = entry.getKey(); + final String expected = entry.getValue(); + final String actual = sortedAttrs.get(key); + Assert.assertEquals(key + " segment values do not match", expected, actual); + } + + int mshSegmentCount = 0; + int obrSegmentCount = 0; + int obxSegmentCount = 0; + int pd1SegmentCount = 0; + int pidSegmentCount = 0; + + for (final Map.Entry entry : sortedAttrs.entrySet()) { + final String entryKey = entry.getKey(); + if (entryKey.startsWith("MSH")) { + mshSegmentCount++; + continue; + } else if (entryKey.startsWith("OBR")) { + obrSegmentCount++; + continue; + } else if (entryKey.startsWith("OBX")) { + obxSegmentCount++; + continue; + } else if (entryKey.startsWith("PD1")) { + pd1SegmentCount++; + continue; + } else if (entryKey.startsWith("PID")) { + pidSegmentCount++; + continue; + } + } + + Assert.assertEquals("Did not have the proper number of MSH segments", 9, mshSegmentCount); + Assert.assertEquals("Did not have the proper number of OBR segments", 14, obrSegmentCount); + Assert.assertEquals("Did not have the proper number of OBX segments", 12, obxSegmentCount); + Assert.assertEquals("Did not have the proper number of PD1 segments", 5, pd1SegmentCount); + Assert.assertEquals("Did not have the proper number of PID segments", 9, pidSegmentCount); + } + }