diff --git a/TEMPLATING.md b/TEMPLATING.md index c301d045..733d7934 100644 --- a/TEMPLATING.md +++ b/TEMPLATING.md @@ -257,6 +257,9 @@ The specification expression has the following format : Example: ``OBX.1 |OBX.2|OBX.3`` , if OBX.1 is null then only OBX.2 will be extracted. * Multiple value extraction - In HL7 several fields can have repeated values, so extract all repetition for that field the spec string should end with *.
Example: ``PID.3 *`` , ``OBX.1 |OBX.2 |OBX.3 *`` +* Preserving white space / empty fields - Blank fields may be used to represent new lines or white space in reports. The user may want to preserve this white space to keep the integrity of the original report. To preserve this white space, the spec string should end with an &. Note that this can be combined (and often will be combined) with the multiple value extraction, either &* or *& is supported.
+ Example: ``OBX.5 *&`` , ``OBX.5 &*``, ``OBX.5 & `` + #### Variable @@ -279,7 +282,7 @@ Engine supports the following condition types: Conditions can be used to choose between multiple sources of data when mapping to a FHIR type. For example, see how `coding` is set in [CodeableConcept.yml](src/main/resources/hl7/datatype/CodeableConcept.yml). `coding` is set by the either coding_1, coding_2, or coding_3 based on the conditions. The last condition that evaluates to true in the list will create the value. #### Different types of expressions -* ResourceExpression : This type of expression is used when a field is a data type defined in one of the [data type templates](src/main/resources/hl7/datatype). These data type templates define different [FHIR data types](https://hl7.org/FHIR/datatypes.html). +* ResourceExpression : This type of expression is used when a field is a data type defined in one of the [data type templates](../master/src/main/resources/hl7/datatype). These data type templates define different [FHIR data types](https://hl7.org/FHIR/datatypes.html). Example: ```yml diff --git a/src/main/java/io/github/linuxforhealth/hl7/data/Hl7DataHandlerUtil.java b/src/main/java/io/github/linuxforhealth/hl7/data/Hl7DataHandlerUtil.java index 0156a658..0341e905 100644 --- a/src/main/java/io/github/linuxforhealth/hl7/data/Hl7DataHandlerUtil.java +++ b/src/main/java/io/github/linuxforhealth/hl7/data/Hl7DataHandlerUtil.java @@ -30,6 +30,10 @@ public static String getStringValue(Object obj) { } public static String getStringValue(Object obj, boolean allComponents) { + return getStringValue(obj,allComponents,". "); + } + + public static String getStringValue(Object obj, boolean allComponents, String separatorString) { if (obj == null) { return null; } @@ -43,20 +47,17 @@ public static String getStringValue(Object obj, boolean allComponents) { returnValue = toStringValue(list.get(0), allComponents); } else if (!list.isEmpty()) { StringBuilder sb = new StringBuilder(); - list.forEach(e -> sb.append(toStringValue(e, allComponents)).append(". ")); + list.forEach(e -> sb.append(toStringValue(e, allComponents)).append(separatorString)); returnValue = StringUtils.strip(sb.toString()); - } else { returnValue = null; } - } else { returnValue = toStringValue(local, allComponents); } return returnValue; - } public static String getTableNumber(Object obj) { @@ -103,7 +104,6 @@ private static String toStringValue(Object local, boolean allComponents) { } - private static String convertVariesDataTypeToString(Object obj, boolean allComponents) { if (obj instanceof Variable) { Variable v = (Variable) obj; @@ -124,9 +124,7 @@ private static String getValueFromComposite(Composite com) { } return StringUtils.stripEnd(sb.toString(), ", "); - } - } diff --git a/src/main/java/io/github/linuxforhealth/hl7/data/Hl7RelatedGeneralUtils.java b/src/main/java/io/github/linuxforhealth/hl7/data/Hl7RelatedGeneralUtils.java index 1f29c0d0..ba800ef2 100644 --- a/src/main/java/io/github/linuxforhealth/hl7/data/Hl7RelatedGeneralUtils.java +++ b/src/main/java/io/github/linuxforhealth/hl7/data/Hl7RelatedGeneralUtils.java @@ -8,11 +8,13 @@ import java.time.temporal.ChronoUnit; import java.time.temporal.Temporal; import java.time.temporal.UnsupportedTemporalTypeException; + import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringTokenizer; import org.hl7.fhir.r4.model.codesystems.EncounterStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import io.github.linuxforhealth.hl7.data.date.DateUtil; @@ -127,5 +129,10 @@ public static String split(Object input, String delimitter, int index) { return null; } + public static String concatenateWithChar(Object input, String delimiterChar) { + String result = Hl7DataHandlerUtil.getStringValue(input, true, delimiterChar); + return result; + } + } diff --git a/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataTypeMapper.java b/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataTypeMapper.java index 5bbd1efe..1abfdbd6 100644 --- a/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataTypeMapper.java +++ b/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataTypeMapper.java @@ -15,7 +15,8 @@ public enum SimpleDataTypeMapper { STRING(SimpleDataValueResolver.STRING), // STRING_ALL(SimpleDataValueResolver.STRING_ALL), // FLOAT(SimpleDataValueResolver.FLOAT), // - + BASE64_BINARY(SimpleDataValueResolver.BASE64_BINARY), + URI(SimpleDataValueResolver.URI_VAL), // URL(SimpleDataValueResolver.STRING), // INSTANT(SimpleDataValueResolver.INSTANT), // diff --git a/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataValueResolver.java b/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataValueResolver.java index 88bb879f..b16c1932 100644 --- a/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataValueResolver.java +++ b/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataValueResolver.java @@ -8,6 +8,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.Base64; import java.util.List; import java.util.Locale; import java.util.Map; @@ -211,6 +212,12 @@ public class SimpleDataValueResolver { }; + public static final ValueExtractor BASE64_BINARY = (Object value) -> { + String val = Hl7DataHandlerUtil.getStringValue(value); + return Base64.getEncoder().encodeToString(val.getBytes()); + + }; + public static final ValueExtractor OBJECT = (Object value) -> { return value; diff --git a/src/main/java/io/github/linuxforhealth/hl7/expression/ExpressionAttributes.java b/src/main/java/io/github/linuxforhealth/hl7/expression/ExpressionAttributes.java index a3935949..66e4804f 100644 --- a/src/main/java/io/github/linuxforhealth/hl7/expression/ExpressionAttributes.java +++ b/src/main/java/io/github/linuxforhealth/hl7/expression/ExpressionAttributes.java @@ -35,7 +35,6 @@ public class ExpressionAttributes { private static final String OBJECT_TYPE = Object.class.getSimpleName(); - // Basic properties of an expression private String name; private String type; @@ -56,8 +55,6 @@ public class ExpressionAttributes { // if valueof attribute ends with * then list of values will be generated private boolean generateMultiple; - - // Property specific to ValueExtractionGeneralExpression private ImmutablePair fetch; @@ -77,22 +74,18 @@ private ExpressionAttributes(Builder exBuilder) { this.condition = ConditionUtil.createCondition(exBuilder.rawCondition); } - this.constants = new HashMap<>(); if (exBuilder.constants != null && !exBuilder.constants.isEmpty()) { this.constants.putAll(exBuilder.constants); } - this.variables = new ArrayList<>(); if (exBuilder.rawVariables != null) { for (Entry e : exBuilder.rawVariables.entrySet()) { this.variables.add(VariableGenerator.parse(e.getKey(), e.getValue())); } - } - this.value = exBuilder.value; this.valueOf = exBuilder.valueOf; this.generateMultiple = exBuilder.generateList; @@ -104,79 +97,56 @@ private ExpressionAttributes(Builder exBuilder) { this.expressionType = ExpressionType.HL7SPEC; } - } public boolean isUseGroup() { return useGroup; } - public String getType() { return type; } - - public String getDefaultValue() { return defaultValue; } - - public boolean isRequired() { return isRequired; } - - public List getSpecs() { return ImmutableList.copyOf(specs); } - - public List getVariables() { return ImmutableList.copyOf(variables); } - - public Condition getFilter() { return condition; } - - public Map getConstants() { return ImmutableMap.copyOf(constants); } - - public boolean isGenerateMultiple() { return generateMultiple; } - - public String getValue() { return value; } - - public ImmutablePair getFetch() { return fetch; } - - public ExpressionType getExpressionType() { return expressionType; } - public String getValueOf() { return valueOf; } @@ -190,23 +160,47 @@ public String getName() { return name; } - public static List getSpecList(String inputString, boolean useGroup) { - final boolean extractMultiple; - String hl7SpecExpression = inputString; - if (StringUtils.endsWith(inputString, "*")) { - hl7SpecExpression = StringUtils.removeEnd(inputString, "*"); + /** + * Extract special chars: + * * indicates to extract fields from multiple entries + * & indicates to retain empty (null) fields + * @param inputString + * @return ExpressionModifiers object with booleans indicating which modifiers were used and the expression after modifiers have been removed + */ + public static final ExpressionModifiers extractExpressionModifiers(String inputString) { + + boolean extractMultiple = false; + boolean retainEmpty = false; + String expression = inputString; + + if (StringUtils.endsWith(expression, "*")) { + expression = StringUtils.removeEnd(expression, "*"); + extractMultiple = true; + } + if (StringUtils.endsWith(expression, "&")) { + expression = StringUtils.removeEnd(expression, "&"); + retainEmpty = true; + } + // Repeat check for asterisk to allow for different order of special chars + if (StringUtils.endsWith(expression, "*")) { + expression = StringUtils.removeEnd(expression, "*"); extractMultiple = true; - } else { - extractMultiple = false; } + expression = StringUtils.strip(expression); + + return new ExpressionModifiers(extractMultiple, retainEmpty, expression); + } + + public static List getSpecList(String inputString, boolean useGroup) { + + ExpressionModifiers exp = extractExpressionModifiers(inputString); - hl7SpecExpression = StringUtils.strip(hl7SpecExpression); List specs = new ArrayList<>(); - if (StringUtils.isNotBlank(hl7SpecExpression)) { - StringTokenizer st = new StringTokenizer(hl7SpecExpression, "|").setIgnoreEmptyTokens(true) + if (StringUtils.isNotBlank(exp.expression)) { + StringTokenizer st = new StringTokenizer(exp.expression, "|").setIgnoreEmptyTokens(true) .setTrimmerMatcher(StringMatcherFactory.INSTANCE.spaceMatcher()); st.getTokenList() - .forEach(s -> specs.add(SpecificationParser.parse(s, extractMultiple, useGroup))); + .forEach(s -> specs.add(SpecificationParser.parse(s, exp.extractMultiple, useGroup, exp.retainEmpty))); } return specs; @@ -227,7 +221,6 @@ private static ImmutablePair getPair(String tok) { } else { return null; } - } @Override @@ -236,7 +229,6 @@ public String toString() { this.toString = ReflectionToStringBuilder.toString(this, ToStringStyle.NO_CLASS_NAME_STYLE, false, false, true, null); } - return this.toString; } @@ -249,6 +241,7 @@ public String toString() { public static class Builder { + private String name; private String type; private String defaultValue; @@ -310,26 +303,22 @@ public Builder withCondition(String rawCondition) { return this; } - public Builder withVars(Map rawVariables) { this.rawVariables = rawVariables; return this; } - public Builder withConstants(Map constants) { this.constants = constants; return this; } public Builder withValueOf(String valueOf) { - this.valueOf = StringUtils.trim(valueOf); if (this.expressionType == null) { this.expressionType = ExpressionType.SIMPLE; } return this; - } public Builder withExpressionType(String expressionType) { @@ -337,8 +326,6 @@ public Builder withExpressionType(String expressionType) { return this; } - - public Builder withValue(String value) { this.value = value; this.expressionType = ExpressionType.SIMPLE; @@ -350,15 +337,24 @@ public Builder withGenerateList(boolean generateList) { return this; } - - public ExpressionAttributes build() { return new ExpressionAttributes(this); } } - - + // Class used when extracting modifiers from the expression, contains the expression after modifiers have been removed and + // booleans indicating which modifiers were in the expression. + public static class ExpressionModifiers { + public boolean extractMultiple = false; // true when * is used in the expression + public boolean retainEmpty = false; // true when & is used in the expression + public String expression = ""; // resulting expression after the modifiers have been removed + + ExpressionModifiers(boolean theExtractMultiple, boolean theRetainEmpty, String theExpression) { + extractMultiple = theExtractMultiple; + retainEmpty = theRetainEmpty; + expression = theExpression; + } + } } diff --git a/src/main/java/io/github/linuxforhealth/hl7/expression/specification/HL7Specification.java b/src/main/java/io/github/linuxforhealth/hl7/expression/specification/HL7Specification.java index a2f1bfb7..7d8958a7 100644 --- a/src/main/java/io/github/linuxforhealth/hl7/expression/specification/HL7Specification.java +++ b/src/main/java/io/github/linuxforhealth/hl7/expression/specification/HL7Specification.java @@ -16,7 +16,6 @@ * Represents HL7 data specification. It defines segment, field, component and subcomponent * names/identifiers that can be used for extracting data. * - * * @author pbhallam */ @@ -27,23 +26,26 @@ public class HL7Specification implements Specification { private int component; private int subComponent; private boolean isExtractMultiple; + private boolean retainEmpty; private String stringRep; private Class sourceInputDataClass = HL7MessageData.class; - public HL7Specification(String segment, String field, int component, int subComponent, - boolean isMultiple) { + public HL7Specification(String segment, String field, int component, int subComponent, boolean isMultiple, boolean retainEmpty) { this.segment = segment; this.field = field; this.component = component; this.subComponent = subComponent; this.isExtractMultiple = isMultiple; + this.retainEmpty = retainEmpty; this.stringRep = getToStringRep(); } + public HL7Specification(String segment, String field, int component, int subComponent, boolean isMultiple) { + this(segment, field, component, subComponent, false, false); + } public HL7Specification(String segment, String field, int component, int subComponent) { - this(segment, field, component, subComponent, false); - + this(segment, field, component, subComponent, false, false); } public String getSegment() { @@ -62,14 +64,9 @@ public int getSubComponent() { return subComponent; } - - - @Override public String toString() { return this.stringRep; - - } private String getToStringRep() { @@ -90,8 +87,6 @@ private String getToStringRep() { } return sb.append("]").toString(); - - } @@ -99,6 +94,10 @@ public boolean isExtractMultiple() { return isExtractMultiple; } + public boolean getRetainEmptyFields() { + return retainEmpty; + } + public Class getSourceInputDataClass() { return sourceInputDataClass; @@ -123,5 +122,4 @@ public EvaluationResult extractMultipleValuesForSpec(InputDataExtractor dataSour } - } diff --git a/src/main/java/io/github/linuxforhealth/hl7/expression/specification/SpecificationParser.java b/src/main/java/io/github/linuxforhealth/hl7/expression/specification/SpecificationParser.java index d38f41a9..90687aa8 100644 --- a/src/main/java/io/github/linuxforhealth/hl7/expression/specification/SpecificationParser.java +++ b/src/main/java/io/github/linuxforhealth/hl7/expression/specification/SpecificationParser.java @@ -10,16 +10,19 @@ public class SpecificationParser { private SpecificationParser() {} - public static Specification parse(String rawSpec, boolean extractMultiple, boolean useGroup) { - if (StringUtils.startsWith(rawSpec, "$")) { + return parse(rawSpec, extractMultiple, useGroup, false); + } + + public static Specification parse(String rawSpec, boolean extractMultiple, boolean useGroup, boolean retainEmpty) { + if (StringUtils.startsWith(rawSpec, "$")) { return new SimpleSpecification(rawSpec, extractMultiple, useGroup); } else { - return getHL7Spec(rawSpec, extractMultiple); + return getHL7Spec(rawSpec, extractMultiple, retainEmpty); } } - private static Specification getHL7Spec(String rawSpec, boolean extractMultiple) { + private static Specification getHL7Spec(String rawSpec, boolean extractMultiple, boolean retainEmpty) { StringTokenizer stk = new StringTokenizer(rawSpec, "."); String segment = null; String field = null; @@ -35,8 +38,6 @@ private static Specification getHL7Spec(String rawSpec, boolean extractMultiple) if (stk.hasNext()) { component = NumberUtils.toInt(stk.nextToken()); } - - } else { field = tok; if (stk.hasNext()) { @@ -48,10 +49,7 @@ private static Specification getHL7Spec(String rawSpec, boolean extractMultiple) } } - - return new HL7Specification(segment, field, component, subComponent, extractMultiple); + return new HL7Specification(segment, field, component, subComponent, extractMultiple, retainEmpty); } - - } diff --git a/src/main/java/io/github/linuxforhealth/hl7/expression/variable/ExpressionVariable.java b/src/main/java/io/github/linuxforhealth/hl7/expression/variable/ExpressionVariable.java index 529dd8a4..e44575ae 100644 --- a/src/main/java/io/github/linuxforhealth/hl7/expression/variable/ExpressionVariable.java +++ b/src/main/java/io/github/linuxforhealth/hl7/expression/variable/ExpressionVariable.java @@ -35,6 +35,11 @@ public ExpressionVariable(String name, String expression, List spec, this.expression = expression; } + public ExpressionVariable(String name, String expression, List spec, + boolean extractMultiple, boolean retainEmpty) { + super(name, spec, extractMultiple, false, retainEmpty); + this.expression = expression; + } @@ -62,7 +67,10 @@ public EvaluationResult extractVariableValue(Map conte } - - - + /** + * @return String representation of expression + */ + public String getExpression() { + return expression; + } } diff --git a/src/main/java/io/github/linuxforhealth/hl7/expression/variable/SimpleVariable.java b/src/main/java/io/github/linuxforhealth/hl7/expression/variable/SimpleVariable.java index bfc51a30..0d5bd951 100644 --- a/src/main/java/io/github/linuxforhealth/hl7/expression/variable/SimpleVariable.java +++ b/src/main/java/io/github/linuxforhealth/hl7/expression/variable/SimpleVariable.java @@ -21,8 +21,7 @@ /** - * Defines Variable object that can be used during the expression evaluation. - * + * Defines Variable object that can be used during the expression evaluation. * * @author pbhallam */ @@ -33,13 +32,16 @@ public class SimpleVariable implements Variable { private List spec; private boolean extractMultiple; private boolean combineMultiple; + private boolean retainEmpty; public SimpleVariable(String name, List spec) { - this(name, spec, false, false); + this(name, spec, false, false, false); } - public SimpleVariable(String name, List spec, boolean extractMultiple, - boolean combineMultiple) { + public SimpleVariable(String name, List spec, boolean extractMultiple, boolean combineMultiple) { + this(name, spec, extractMultiple, combineMultiple, false); + } + public SimpleVariable(String name, List spec, boolean extractMultiple, boolean combineMultiple, boolean retainEmpty) { this.name = name; this.spec = new ArrayList<>(); if (spec != null && !spec.isEmpty()) { @@ -47,6 +49,7 @@ public SimpleVariable(String name, List spec, boolean extractMultiple, } this.extractMultiple = extractMultiple; this.combineMultiple = combineMultiple; + this.retainEmpty = retainEmpty; } @Override @@ -59,15 +62,11 @@ public String getType() { return OBJECT_TYPE; } - public String getName() { return name; } - - - // resolve variable value @Override @@ -87,9 +86,7 @@ public EvaluationResult extractVariableValue(Map conte } else { result = null; } - return result; - } @@ -123,7 +120,7 @@ protected List getValuesFromSpecs(Map specs = getTokens(values[0]); - return new ExpressionVariable(varName, values[1], specs, extractMultiple); + List specs = getTokens(exp.expression); + return new ExpressionVariable(varName, values[1], specs, exp.extractMultiple, exp.retainEmpty); } throw new IllegalArgumentException("rawVariable not in correct format "); } else if (StringUtils.contains(rawVariable, ",")) { String[] values = rawVariable.split(",", 2); if (values.length == COMPONENT_LENGTH_FOR_VAR_EXPRESSION) { List specs = getTokens(values[1]); - return new DataTypeVariable(varName, values[0], specs, extractMultiple); + return new DataTypeVariable(varName, values[0], specs, exp.extractMultiple); } throw new IllegalArgumentException("rawVariable not in correct format "); } else { @@ -52,7 +52,7 @@ public static Variable parse(String varName, String variableExpression) { combineValues = true; } List specs = getTokens(rawVariable); - return new SimpleVariable(varName, specs, extractMultiple, combineValues); + return new SimpleVariable(varName, specs, exp.extractMultiple, combineValues, exp.retainEmpty); } } diff --git a/src/main/java/io/github/linuxforhealth/hl7/message/HL7MessageData.java b/src/main/java/io/github/linuxforhealth/hl7/message/HL7MessageData.java index 192fc553..71077488 100644 --- a/src/main/java/io/github/linuxforhealth/hl7/message/HL7MessageData.java +++ b/src/main/java/io/github/linuxforhealth/hl7/message/HL7MessageData.java @@ -61,7 +61,6 @@ public EvaluationResult extractMultipleValuesForSpec(Specification spec, Object hl7object = null; if (valuefromVariables != null) { hl7object = valuefromVariables.getValue(); - } if (hl7object instanceof List) { @@ -72,6 +71,10 @@ public EvaluationResult extractMultipleValuesForSpec(Specification spec, extractedValues.addAll((List) result); } else if (result != null) { extractedValues.add(result); + } else if (result==null && hl7spec.getRetainEmptyFields() ) { + // In this case, we need to preserve empty / blank fields. This is specified by '&' in the config. + // so add an empty string to the result array to achieve this. + extractedValues.add(""); } } return EvaluationResultFactory.getEvaluationResult(extractedValues); @@ -79,23 +82,18 @@ public EvaluationResult extractMultipleValuesForSpec(Specification spec, } else { return EvaluationResultFactory.getEvaluationResult(extractValue(hl7spec, hl7object)); } - - } - private Object extractValue(HL7Specification hl7spec, Object obj) { EvaluationResult res = null; try { if (obj instanceof Segment) { res = extractSpecValuesFromSegment(obj, hl7spec); - } else if (obj instanceof Type) { res = extractSpecValuesFromField(obj, hl7spec); } else if (obj == null) { res = extractSpecValues(hl7spec); - } } catch (DataExtractionException e) { LOGGER.warn("cannot extract value for variable {} ", hl7spec, e); @@ -105,7 +103,6 @@ private Object extractValue(HL7Specification hl7spec, Object obj) { } else { return null; } - } @@ -140,7 +137,6 @@ private EvaluationResult extractSpecValuesFromSegment(Object obj, HL7Specificati } else { return null; } - } else { return EvaluationResultFactory.getEvaluationResult(obj); } @@ -148,7 +144,6 @@ private EvaluationResult extractSpecValuesFromSegment(Object obj, HL7Specificati private EvaluationResult extractSpecValuesFromField(Object obj, HL7Specification hl7spec) { - if (hl7spec.getComponent() >= 0) { ParsingResult res; if (hl7spec.getSubComponent() >= 0) { @@ -165,9 +160,6 @@ private EvaluationResult extractSpecValuesFromField(Object obj, HL7Specification } else { return EvaluationResultFactory.getEvaluationResult(obj); } - - - } @@ -187,7 +179,6 @@ public EvaluationResult evaluateJexlExpression(String expression, resolvedVariables.forEach((key, value) -> localContext.put(key, value.getValue())); Object obj = JEXL.evaluate(trimedJexlExp, localContext); return EvaluationResultFactory.getEvaluationResult(obj); - } @@ -203,7 +194,6 @@ public String getId() { } - @Override public EvaluationResult extractValueForSpec(Specification spec, Map contextValues) { @@ -213,9 +203,9 @@ public EvaluationResult extractValueForSpec(Specification spec, } else { return new EmptyEvaluationResult(); } - } + private static Object getSingleValue(Object object) { if (object instanceof List) { List value = (List) object; @@ -224,11 +214,9 @@ private static Object getSingleValue(Object object) { } else { return value.get(0); } - } return object; } - } diff --git a/src/main/resources/hl7/codesystem/CodingSystemMapping.yml b/src/main/resources/hl7/codesystem/CodingSystemMapping.yml index 8c9bba32..e213d3a2 100644 --- a/src/main/resources/hl7/codesystem/CodingSystemMapping.yml +++ b/src/main/resources/hl7/codesystem/CodingSystemMapping.yml @@ -4464,3 +4464,8 @@ description: "United States National Provider Identifier" url: "http://hl7.org/fhir/sid/us-npi" oid: "urn:oid:2.16.840.1.113883.4.6" + +- id: "AllLanguages" + description: "languages" + url: "http://hl7.org/fhir/ValueSet/all-languages" + oid: "urn:oid:2.16.840.1.113883.4.642.3.21" \ No newline at end of file diff --git a/src/main/resources/hl7/datatype/Attachment.yml b/src/main/resources/hl7/datatype/Attachment.yml new file mode 100644 index 00000000..0c58b58b --- /dev/null +++ b/src/main/resources/hl7/datatype/Attachment.yml @@ -0,0 +1,48 @@ +# +# (C) Copyright IBM Corp. 2020 +# +# SPDX-License-Identifier: Apache-2.0 +# +# Represents attachment +# Relevant spec: http://hl7.org/fhir/datatypes.html#Attachment +--- +# From the spec: The contentType element SHALL always be populated when an Attachment contains data. Identifies the type of the data in the attachment, +# as a mime type. Common mime types are available at https://terminology.hl7.org/2.1.0/CodeSystem-v2-0834.html +contentType: + type: STRING + valueOf: $mime + +# From the spec: The human language of the content. The value can be any valid value according to BCP 47. +language: + type: STRING + valueOf: $language + +# From the spec: The actual data of the attachment - a sequence of bytes, base64 encoded. +data: + type: BASE64_BINARY + valueOf: $data + +# The hl7v2-fhir-converter will not set the url field. Since the converter is returning only json (not creating any files) +# and putting information in the data field, the uri field will not be set. If desired, the user could set the url field +# to link to a file in the resulting json. +#url: + +# The hl7v2-fhir-converter will not set the size field, since it isn't relevant without the url field. +# See the spec at http://hl7.org/fhir/datatypes-definitions.html#Attachment.size which says: The number of bytes is +# redundant if the data is provided as a base64binary, but is useful if the data is provided as a url reference. +#size: + +# The hl7v2-fhir-converter will not set the hash field, since it isn't relevant without the url field. +# See the spec at http://hl7.org/fhir/datatypes.html#Attachment which says: The hash is included so that applications +# can verify that the content returned by the URL has not changed. +#hash: + +# From the spec: A label or set of text to display in place of the data. +title: + type: STRING + valueOf: $title + +# From the spec: The date that the attachment was first created. +creation: + type: DATE_TIME + valueOf: $date \ No newline at end of file diff --git a/src/main/resources/hl7/message/ORU_R01.yml b/src/main/resources/hl7/message/ORU_R01.yml index c5588d72..ded0045f 100644 --- a/src/main/resources/hl7/message/ORU_R01.yml +++ b/src/main/resources/hl7/message/ORU_R01.yml @@ -41,8 +41,7 @@ resources: additionalSegments: - .OBR - .OBSERVATION.NTE - - + - resourceName: Specimen segment: .SPECIMEN.SPM group: PATIENT_RESULT.ORDER_OBSERVATION @@ -61,5 +60,4 @@ resources: additionalSegments: - .ORC - .NTE - - + - .OBSERVATION.OBX \ No newline at end of file diff --git a/src/main/resources/hl7/resource/DiagnosticReport.yml b/src/main/resources/hl7/resource/DiagnosticReport.yml index b04d993f..d4ec15c0 100644 --- a/src/main/resources/hl7/resource/DiagnosticReport.yml +++ b/src/main/resources/hl7/resource/DiagnosticReport.yml @@ -19,6 +19,7 @@ status: type: DIAGNOSTIC_REPORT_STATUS valueOf: OBR.25 expressionType: HL7Spec + category: valueOf: datatype/CodeableConcept generateList: true @@ -72,3 +73,26 @@ result: expressionType: resource specs: $Observation useGroup: true + +presentedForm: + valueOf: datatype/Attachment + expressionType: resource + # This merges all the OBX lines together when there is no id (obx3) and the message has only type 'TX' (obx2). + # Messages with mixed types of OBX segments will not have a presentedForm attachment created. + condition: $obx2 EQUALS TX && $obx3 NULL + vars: + # This concatenates all OBX-5 lines together (the asterisk) and preserves blank lines (the ampersand). Multiple lines are concatenated with a tilde. + data: OBX.5 *&, GeneralUtils.concatenateWithChar(data, '~') + title: OBR.4.2 + date: OBX.14 + mime: $code + language: $code2 + obx2: STRING, OBX.2 + obx3: STRING, OBX.3 + constants: + system: 'http://terminology.hl7.org/CodeSystem/v2-0834' + code: 'text' + display: 'Text data' + system2: 'http://hl7.org/fhir/ValueSet/all-languages' + code2: 'en' + display2: 'English' \ No newline at end of file diff --git a/src/test/java/io/github/linuxforhealth/hl7/expression/varable/VariableGeneratorTest.java b/src/test/java/io/github/linuxforhealth/hl7/expression/varable/VariableGeneratorTest.java new file mode 100644 index 00000000..b4660be8 --- /dev/null +++ b/src/test/java/io/github/linuxforhealth/hl7/expression/varable/VariableGeneratorTest.java @@ -0,0 +1,57 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ +package io.github.linuxforhealth.hl7.expression.varable; + +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Test; +import io.github.linuxforhealth.hl7.expression.variable.DataTypeVariable; +import io.github.linuxforhealth.hl7.expression.variable.ExpressionVariable; +import io.github.linuxforhealth.hl7.expression.variable.VariableGenerator; + +public class VariableGeneratorTest { + + /** + * + * Test that parse expressions with * at the end works + * + * var1: STRING, OBX-5 * + * + * @throws IOException + */ + @Test + public void parseDataTypeVariableWithAsterixAtEnd() throws IOException { + String varName = "var1"; + String variableExpression = "STRING, OBX-5 *"; + DataTypeVariable v = (DataTypeVariable) VariableGenerator.parse(varName, variableExpression); + + Assert.assertTrue("Variable name not set correctly", v.getVariableName().equalsIgnoreCase(varName)); + Assert.assertTrue("Variable spec not set correctly", v.getSpec().get(0).equalsIgnoreCase("OBX-5")); + Assert.assertTrue("Variable type not set correctly", v.getValueType().equalsIgnoreCase("STRING")); + Assert.assertTrue("Variable extract multiple should be true", v.extractMultiple()); + } + + /** + * + * Test that parse expressions with * and call to evaluate java function + * + * var1: OBX.5 *, GeneralUtils.testFunction(x, y) + * + * @throws IOException + */ + @Test + public void parseExpressionVariableWithAsterixAndEvaluatingJavaFunction() throws IOException { + String varName = "var1"; + String variableExpression = "OBX.5 *, GeneralUtils.testFunction(x, y)"; + ExpressionVariable v = (ExpressionVariable) VariableGenerator.parse(varName, variableExpression); + + Assert.assertTrue("Variable name not set correctly", v.getVariableName().equalsIgnoreCase(varName)); + Assert.assertTrue("Variable spec not set correctly", v.getSpec().get(0).equalsIgnoreCase("OBX.5")); + Assert.assertTrue("Variable expression not set correctly", v.getExpression().equalsIgnoreCase(" GeneralUtils.testFunction(x, y)")); + Assert.assertTrue("Variable extract multiple should be true", v.extractMultiple()); + } +} \ No newline at end of file diff --git a/src/test/java/io/github/linuxforhealth/hl7/message/Hl7ORUMessageTest.java b/src/test/java/io/github/linuxforhealth/hl7/message/Hl7ORUMessageTest.java index f8c2571b..5617a01c 100644 --- a/src/test/java/io/github/linuxforhealth/hl7/message/Hl7ORUMessageTest.java +++ b/src/test/java/io/github/linuxforhealth/hl7/message/Hl7ORUMessageTest.java @@ -6,10 +6,20 @@ package io.github.linuxforhealth.hl7.message; import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; import java.io.IOException; +import java.time.ZoneId; +import java.util.Base64; +import java.util.Calendar; +import java.util.Date; import java.util.List; +import java.util.TimeZone; import java.util.stream.Collectors; + + import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Attachment; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.Bundle.BundleType; @@ -17,6 +27,7 @@ import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.ResourceType; +import org.junit.Assert; import org.junit.Test; import io.github.linuxforhealth.fhir.FHIRContext; import io.github.linuxforhealth.hl7.ConverterOptions; @@ -26,7 +37,11 @@ public class Hl7ORUMessageTest { private static FHIRContext context = new FHIRContext(); private static final ConverterOptions OPTIONS = new Builder().withValidateResource().build(); - + private static final ConverterOptions OPTIONS_PRETTYPRINT = new Builder() + .withBundleType(BundleType.COLLECTION) + .withValidateResource() + .withPrettyPrint() + .build(); @Test public void test_oru() throws IOException { @@ -128,9 +143,6 @@ public void test_oru_multiple() throws IOException { DiagnosticReport enc = getResource(diagnosticresource.get(0)); Reference ref = enc.getSubject(); assertThat(ref.isEmpty()).isFalse(); - - - } @Test @@ -163,12 +175,142 @@ public void test_oru_spm() throws IOException { assertThat(spmRef.get(0).isEmpty()).isFalse(); } + /** + * + * ORU messages with an OBR and multiple OBX segments that do not have an id represent a report + * + * The OBX segments should be grouped together and added the presentedForm as an attachment for the diagnostic report + * @throws IOException + */ + @Test + public void multipleOBXWithNoId() throws IOException { + HL7ToFHIRConverter ftv = new HL7ToFHIRConverter(); + String json = ftv.convert(new File("../hl7v2-fhir-converter/src/test/resources/ORU-multiline-short.hl7"), OPTIONS_PRETTYPRINT); + + //Verify conversion + FHIRContext context = new FHIRContext(); + IBaseResource bundleResource = context.getParser().parseResource(json); + Bundle b = (Bundle) bundleResource; + + Assert.assertTrue("Bundle type not expected", b.getType() == BundleType.COLLECTION); + b.getId(); + b.getMeta().getLastUpdated(); + + List e = b.getEntry(); + + List patientResource = e.stream() + .filter(v -> ResourceType.Patient == v.getResource().getResourceType()) + .map(BundleEntryComponent::getResource).collect(Collectors.toList()); + assertThat(patientResource).hasSize(1); + + List organizationResource = e.stream() + .filter(v -> ResourceType.Organization == v.getResource().getResourceType()) + .map(BundleEntryComponent::getResource).collect(Collectors.toList()); + assertThat(organizationResource).hasSize(1); + + List messageHeader = e.stream() + .filter(v -> ResourceType.MessageHeader == v.getResource().getResourceType()) + .map(BundleEntryComponent::getResource).collect(Collectors.toList()); + assertThat(messageHeader).hasSize(1); + + //Verify no observations are created + List obsResource = e.stream() + .filter(v -> ResourceType.Observation == v.getResource().getResourceType()) + .map(BundleEntryComponent::getResource).collect(Collectors.toList()); + assertThat(obsResource).hasSize(0); + + //Verify Diagnostic Report is created as expected + List reportResource = e.stream() + .filter(v -> ResourceType.DiagnosticReport == v.getResource().getResourceType()) + .map(BundleEntryComponent::getResource).collect(Collectors.toList()); + assertThat(reportResource).hasSize(1); + + DiagnosticReport report = (DiagnosticReport) reportResource.get(0); + + List attachments = report.getPresentedForm(); + Assert.assertTrue("Unexpected number of attachments", attachments.size() == 1); + + //Verify attachment to diagnostic report + Attachment a = attachments.get(0); + Assert.assertTrue("Incorrect content type", a.getContentType().equalsIgnoreCase("text")); + Assert.assertTrue("Incorrect language", a.getLanguage().equalsIgnoreCase("en")); + + //Verify data attachment after decoding + String decoded = new String(Base64.getDecoder().decode(a.getDataElement().getValueAsString())); + Assert.assertTrue("Incorrect data", decoded.equals("~[PII] Emergency Department~ED Encounter Arrival Date: [ADDRESS] [PERSONALNAME]:~")); + + Assert.assertTrue("Incorrect title", a.getTitle().equalsIgnoreCase("ECHO CARDIOGRAM COMPLETE")); + + //Verify creation data is persisted correctly - 2020-08-02T12:44:55+08:00 + Calendar c = Calendar.getInstance(); + c.clear(); // needed to completely clear out calendar object + c.set(2020, 7, 2, 12, 44, 55); + c.setTimeZone(TimeZone.getTimeZone(ZoneId.of("+08:00"))); + + Date d = c.getTime(); + Assert.assertTrue("Incorrect creation date", a.getCreation().equals(d)); + + } + + /** + * + * Verifies ORU messages with mixed OBX types + * + * @throws IOException + */ + @Test + public void multipleOBXWithMixedType() throws IOException { + HL7ToFHIRConverter ftv = new HL7ToFHIRConverter(); + String json = ftv.convert(new File("../hl7v2-fhir-converter/src/test/resources/ORU-multiline-short-mixed.hl7"), OPTIONS_PRETTYPRINT); + + //Verify conversion + FHIRContext context = new FHIRContext(); + IBaseResource bundleResource = context.getParser().parseResource(json); + Bundle b = (Bundle) bundleResource; + + Assert.assertTrue("Bundle type not expected", b.getType() == BundleType.COLLECTION); + b.getId(); + b.getMeta().getLastUpdated(); + + List e = b.getEntry(); + + List patientResource = e.stream() + .filter(v -> ResourceType.Patient == v.getResource().getResourceType()) + .map(BundleEntryComponent::getResource).collect(Collectors.toList()); + assertThat(patientResource).hasSize(1); + + List organizationResource = e.stream() + .filter(v -> ResourceType.Organization == v.getResource().getResourceType()) + .map(BundleEntryComponent::getResource).collect(Collectors.toList()); + assertThat(organizationResource).hasSize(1); + + List messageHeader = e.stream() + .filter(v -> ResourceType.MessageHeader == v.getResource().getResourceType()) + .map(BundleEntryComponent::getResource).collect(Collectors.toList()); + assertThat(messageHeader).hasSize(1); + + //Verify one observations is created + List obsResource = e.stream() + .filter(v -> ResourceType.Observation == v.getResource().getResourceType()) + .map(BundleEntryComponent::getResource).collect(Collectors.toList()); + assertThat(obsResource).hasSize(1); + + //Verify Diagnostic Report is created as expected + List reportResource = e.stream() + .filter(v -> ResourceType.DiagnosticReport == v.getResource().getResourceType()) + .map(BundleEntryComponent::getResource).collect(Collectors.toList()); + assertThat(reportResource).hasSize(1); + + DiagnosticReport report = (DiagnosticReport) reportResource.get(0); + + //No attachment created since OBX with TX and no id is not first + List attachments = report.getPresentedForm(); + Assert.assertTrue("Unexpected number of attachments", attachments.size() == 0); + } + private static DiagnosticReport getResource(Resource resource) { String s = context.getParser().encodeResourceToString(resource); Class klass = DiagnosticReport.class; return (DiagnosticReport) context.getParser().parseResource(klass, s); } - - - } diff --git a/src/test/resources/ORU-multiline-short-mixed.hl7 b/src/test/resources/ORU-multiline-short-mixed.hl7 new file mode 100644 index 00000000..081ecc66 --- /dev/null +++ b/src/test/resources/ORU-multiline-short-mixed.hl7 @@ -0,0 +1,9 @@ +MSH|^~\&|WHI_Automation|IBM_Toronto_Lab|IMAGING_REPORT|Hartland|20200802124455||ORU^R01^ORU_R01|MSGID00231|T|2.6||||||||||||^4086::132:2A57:3C28^IPv6 +PID|1||0d70c6c8^^^MRN||Patient^Autogenerated||19630306|M||Caucasian|^^^^L6G 1C7~^^^ON~^^Unionville~&Warden Av~8200~^^^^^^B||^^^^^^4042808~^^^^^905~^^^^001|||Married|Baptist|Account_0d70c6c8 +NTE|1||Created for MRN: 0d70c6c8 +PV1|1|I|^^^Toronto^^^8200 Warden Av|EM|||2905^Langa^Albert^J^IV||0007^SINGH^BALDEV||||||||5755^Kuczma^Sean^^Jr||Visit_0d70c6c8|||||||||||||||||||||||||20200802124455||||||||ABC +OBR|1|PON_0d70c6c8^LAB|FON_0d70c6c8^LAB|1487^ECHO CARDIOGRAM COMPLETE||20200802124455|20200802124455|||||||||OP_0d70c6c8^SINGH^BALDEV||||||||CT|F|||COP_0d70c6c8^GARCIA^LUIS +OBX|1|ST|12345||||||||F|||20200802124455 +OBX|2|TX|||||||||F|||20200802124455 +OBX|3|TX|||[PII] Emergency Department||||||F|||20200802124455 +OBX|4|TX|||ED Encounter Arrival Date: [ADDRESS] [PERSONALNAME]:||||||F|||20200802124455 \ No newline at end of file diff --git a/src/test/resources/ORU-multiline-short.hl7 b/src/test/resources/ORU-multiline-short.hl7 new file mode 100644 index 00000000..3f313fdb --- /dev/null +++ b/src/test/resources/ORU-multiline-short.hl7 @@ -0,0 +1,8 @@ +MSH|^~\&|WHI_Automation|IBM_Toronto_Lab|IMAGING_REPORT|Hartland|20200802124455||ORU^R01^ORU_R01|MSGID00231|T|2.6||||||||||||^4086::132:2A57:3C28^IPv6 +PID|1||0d70c6c8^^^MRN||Patient^Autogenerated||19630306|M||Caucasian|^^^^L6G 1C7~^^^ON~^^Unionville~&Warden Av~8200~^^^^^^B||^^^^^^4042808~^^^^^905~^^^^001|||Married|Baptist|Account_0d70c6c8 +NTE|1||Created for MRN: 0d70c6c8 +PV1|1|I|^^^Toronto^^^8200 Warden Av|EM|||2905^Langa^Albert^J^IV||0007^SINGH^BALDEV||||||||5755^Kuczma^Sean^^Jr||Visit_0d70c6c8|||||||||||||||||||||||||20200802124455||||||||ABC +OBR|1|PON_0d70c6c8^LAB|FON_0d70c6c8^LAB|1487^ECHO CARDIOGRAM COMPLETE||20200802124455|20200802124455|||||||||OP_0d70c6c8^SINGH^BALDEV||||||||CT|F|||COP_0d70c6c8^GARCIA^LUIS +OBX|1|TX|||||||||F|||20200802124455 +OBX|2|TX|||[PII] Emergency Department||||||F|||20200802124455 +OBX|3|TX|||ED Encounter Arrival Date: [ADDRESS] [PERSONALNAME]:||||||F|||20200802124455 \ No newline at end of file