diff --git a/TECHNIQUES.md b/TECHNIQUES.md index 62c9dcdb..bd1672e7 100644 --- a/TECHNIQUES.md +++ b/TECHNIQUES.md @@ -120,7 +120,7 @@ The rules for determining a time zone for a date time value are: Hints about the ways syntax and references work in the YAML files -### Condition test variables, not templates +### Condition test variables, not segment fields Testing the segment fields directly in conditions doesn't work. Instead you must create a var for the template field and test the var. @@ -150,6 +150,30 @@ telecom_1: use: "work" ``` +**Note**: You can now test fields, components and sub-components +from custom segments (Segments beginning with `Z`): + +```yml +type_1: + condition: $zsc322 EQUALS H1 || $zsc322 EQUALS H2 + generateList: false + expressionType: HL7Spec + valueOf: $code + vars: + zsc322: ZSC.3.2.2 + constants: + code: allergy + +type_2: + condition: $zsc322 NOT_EQUALS H1 && $zsc322 NOT_EQUALS H2 + generateList: false + expressionType: HL7Spec + valueOf: $code + vars: + zsc322: ZSC.3.2.2 + constants: + code: intolerance +``` ### Referencing resources Resources are referenced (linked) in one of two ways: @@ -260,4 +284,66 @@ fundingSource: - valueOf: datatype/CodeableConcept expressionType: resource specs: OBX.5 -``` \ No newline at end of file +``` + +## Conditional Templates + +Sometimes, when dealing with custom HL7 segments the correct FHIR resource for the segment differs +depending upon some value in the segment. For example, in our ZAL custom Alert segment field `ZAL.2.1` denotes the Alert Category, +and when the value is one of `A1`, `A3`, `H2` or `H4` then the correct FHIR resource is `AllergyIntolerance`; for all other +alert category values the correct FHIR resource is `Flag` + +Two resources template entries with suitable `condition` expressions will direct each `ZAL` segment to its correct resource template. + +```yml +resources: + - resourceName: AllergyIntolerance + segment: ZAL + resourcePath: resource/AllergyIntoleranceZAL + repeats: true + condition: ZAL.2.1 IN [A1, A3, H2, H4] ## Some of our custom ZAL segments are AllergyIntolerance + additionalSegments: + + - resourceName: Flag + segment: ZAL + resourcePath: resource/FlagZAL + repeats: true + condition: ZAL.2.1 NOT_IN [A1, A3, H2, H4] ## The rest of our custom ZAL segments are more general alert Flags + additionalSegments: + +``` +The grammar for the condition field is as follows: + +``` + EQUALS | NOT_EQUALS | IN | NOT_IN | NULL | NOT_NULL | [ ... ] +``` +**Notes:** + * hl7spec uses the same dot notation as expression syntax inside templates and can be of the folowing forms: + - SEGMENT + - SEGMENT.FIELD + - SEGMENT.FIELD.COMPONENT + - SEGMENT.FIELD.COMPONENT.SUBCOMPONENT + - SEGMENT.FIELD(REPETITION) + - SEGMENT.FIELD(REPETITION).COMPONENT + - SEGMENT.FIELD(REPETITION).COMPONENT.SUBCOMPONENT + + `ZAL`, `ZAL.2`, `ZAL.2.1`, `ZAL.2.1.2`, `PID.14(1).2` & `PID.14(1).2.1` are all valid values for hl7spec. + + * The SEGMENT part of hl7spec **MUST** match the value of the `segment` field. + + * EQUALS and NOT_EQUALS expressions only accept a single value on the right-hand side of the expression. + + * IN and NOT_IN expressions only accept a list of values, delimited by comma, in square brackets on the right-hand side of the expression. + + * NULL and NOT_NULL do not accept any value. + + * The condition expression cannot be much more complex, as there are no context variables available at evaluation time. + +**Examples**: + * `PID.5.1.2 EQUALS van` + * `ZAL.2.1.3 NOT_NULL` + * `ZAL.2(1).1 NULL` + * `ZAL.3.1 IN [A3, A4, H1, H3, FA, DA]` + * `ZAL.3.1 NOT_IN [A2, F3, DA]` + * `ZAL.2 EQUALS A4` + * `ZAL NOT_EQUALS H2` diff --git a/TEMPLATING.md b/TEMPLATING.md index 5405fb48..30da05c2 100644 --- a/TEMPLATING.md +++ b/TEMPLATING.md @@ -14,6 +14,8 @@ A HL7 message template maps one or more HL7 segments to a FHIR resource using th resourcePath: [REQUIRED] repeats: [DEFAULT false] isReferenced: [DEFAULT false] + ignoreEmpty: [DEFAULT false] + condition: [DEFAULT empty] additionalSegments: [DEFAULT empty] ``` @@ -24,6 +26,8 @@ A HL7 message template maps one or more HL7 segments to a FHIR resource using th | resourcePath | Required | Relative path to the resource template. Example: resource/Patient | | repeats | Default: false | Indicates if a repeating HL7 segment will generate multiple FHIR resources. | | isReferenced | Default: false | Indicates if the FHIR Resource is referenced by other FHIR resources. | +| ignoreEmpty | Default: false | Indicates if an empty HL7 segment will NOT generate the matching (almost empty) FHIR resource | +| condition | Default: empty | Indicates that the segment must satisfy the given condition for it to generate the matching FHIR resource. see [Techniques: Conditional Templates](TECHNIQUES.md#Conditional-Templates) | | group | Default: empty | Base group from which the segment and additionalSegments are specified. | additionalSegments | Default: empty | List of additional HL7 segment names required to complete the FHIR resource mapping. | @@ -61,8 +65,22 @@ resources: segment: AL1 resourcePath: resource/AllergyIntolerance repeats: true + ignoreEmpty: true ## Sometimes AL1 segments arrive empty additionalSegments: + - resourceName: AllergyIntolerance + segment: ZAL + resourcePath: resource/AllergyIntoleranceZAL + repeats: true + condition: ZAL.2.1 IN [A1, A3, H2, H4] ## Some of our custom ZAL segments are AllergyIntolerance + additionalSegments: + + - resourceName: Flag + segment: ZAL + resourcePath: resource/FlagZAL + repeats: true + condition: ZAL.2.1 NOT_IN [A1, A3, H2, H4] ## The rest of our custom ZAL segments are more general alert Flags + additionalSegments: ``` @@ -259,6 +277,7 @@ The specification expression has the following format : - SEGMENT - SEGMENT.FIELD - SEGMENT.FIELD.COMPONENT + - SEGMENT.FIELD.COMPONENT.SUBCOMPONENT - FIELD - FIELD.COMPONENT - FIELD.COMPONENT.SUBCOMPONENT
diff --git a/src/main/java/io/github/linuxforhealth/api/FHIRResourceTemplate.java b/src/main/java/io/github/linuxforhealth/api/FHIRResourceTemplate.java index 2c705d43..df133cae 100644 --- a/src/main/java/io/github/linuxforhealth/api/FHIRResourceTemplate.java +++ b/src/main/java/io/github/linuxforhealth/api/FHIRResourceTemplate.java @@ -49,6 +49,17 @@ public interface FHIRResourceTemplate { */ boolean isReferenced(); + /** + * If this resource is to ignore empty source segments + * + * @return True/False + */ + boolean ignoreEmpty(); - + /** + * If this resource is only to be applied sometimes + * + * @return Simple condition expression which must be True for the template to be applied; e.g. ZAL.1 IN [A3, A4, H1, H3] + */ + String conditionExpression(); } diff --git a/src/main/java/io/github/linuxforhealth/api/ResourceCondition.java b/src/main/java/io/github/linuxforhealth/api/ResourceCondition.java new file mode 100644 index 00000000..32b3cedd --- /dev/null +++ b/src/main/java/io/github/linuxforhealth/api/ResourceCondition.java @@ -0,0 +1,25 @@ +/* + * (c) Copyright Te Whatu Ora, Health New Zealand, 2023 + * + * SPDX-License-Identifier: Apache-2.0 + */ +package io.github.linuxforhealth.api; + +/** + * Resource Condition allows for conditional application of ResourceTemplate to a particular message + * + * @author Stuart McGrigor + */ +public interface ResourceCondition { + + + /** + * Evaluates the condition against the HL7 Message + * + * @param context {@link EvaluationResult} representing the HL7 Segment being evaluated + * @return true if condition is satisfied by the HL7 Segment being evaluated, otherwise returns false; + */ + default boolean isConditionSatisfied(InputDataExtractor ide, EvaluationResult context) { + return true; + } +} diff --git a/src/main/java/io/github/linuxforhealth/core/expression/SimpleEvaluationResult.java b/src/main/java/io/github/linuxforhealth/core/expression/SimpleEvaluationResult.java index a2308cbf..83c0f3ae 100644 --- a/src/main/java/io/github/linuxforhealth/core/expression/SimpleEvaluationResult.java +++ b/src/main/java/io/github/linuxforhealth/core/expression/SimpleEvaluationResult.java @@ -57,6 +57,7 @@ public SimpleEvaluationResult(V value, List additionalResources) Preconditions.checkArgument(additionalResources != null, "additionalResources cannot be null"); this.value = value; + this.klass = value.getClass(); this.klassName = DataTypeUtil.getDataType(value); this.additionalResources = new ArrayList<>(); diff --git a/src/main/java/io/github/linuxforhealth/core/expression/condition/ConditionPredicateEnum.java b/src/main/java/io/github/linuxforhealth/core/expression/condition/ConditionPredicateEnum.java index 7fbb1931..32d3001e 100644 --- a/src/main/java/io/github/linuxforhealth/core/expression/condition/ConditionPredicateEnum.java +++ b/src/main/java/io/github/linuxforhealth/core/expression/condition/ConditionPredicateEnum.java @@ -60,8 +60,11 @@ public static ConditionPredicateEnum getConditionPredicate(String conditionOpera String klassSimpleName) { // Append the predicate if not already present // Some classes need a string predicate - String klassAdjustedName = klassSimpleName.equalsIgnoreCase("ST") || klassSimpleName.equalsIgnoreCase("IS") || klassSimpleName.equalsIgnoreCase("NULLDT") ? "STRING" : klassSimpleName.toUpperCase(); - String enumName = conditionOperator.endsWith(klassAdjustedName) ? conditionOperator : conditionOperator + "_" + klassSimpleName; + // - Fields, Components & SubComponents from custom segments are in Varies or GenericPrimitive objects and are assumed to be STRING values + String klassAdjustedName = klassSimpleName.equals("Varies") || klassSimpleName.equals("GenericPrimitive") || klassSimpleName.equalsIgnoreCase("ST") || klassSimpleName.equalsIgnoreCase("IS") || klassSimpleName.equalsIgnoreCase("NULLDT") ? "STRING" : klassSimpleName.toUpperCase(); + + // Gotta be using the adjusted name - or what's the point of the adjustment? + String enumName = conditionOperator.endsWith(klassAdjustedName) ? conditionOperator : conditionOperator + "_" + klassAdjustedName; return EnumUtils.getEnumIgnoreCase(ConditionPredicateEnum.class, enumName); } diff --git a/src/main/java/io/github/linuxforhealth/core/expression/condition/SimpleBiCondition.java b/src/main/java/io/github/linuxforhealth/core/expression/condition/SimpleBiCondition.java index 3aad4320..3eebdaac 100644 --- a/src/main/java/io/github/linuxforhealth/core/expression/condition/SimpleBiCondition.java +++ b/src/main/java/io/github/linuxforhealth/core/expression/condition/SimpleBiCondition.java @@ -32,11 +32,21 @@ public SimpleBiCondition(String var1, String var2, String conditionOperator) { @Override public boolean test(Map contextVariables) { Object var1Value = null; + String var1Type = null; EvaluationResult variable1; if (VariableUtils.isVar(var1)) { variable1 = contextVariables.get(VariableUtils.getVarName(var1)); + + if(VariableUtils.getVarName(var1).equals("zal21")) + System.out.println("Hello Stuey zal21"); + if (variable1 != null && !variable1.isEmpty()) { var1Value = variable1.getValue(); + + // generic variables (derived from fields,components & subComponents in custom segments) will have UNKNOWN type; + var1Type = variable1.getIdentifier(); + if(var1Type.equals("UNKNOWN") && var1Value != null) + var1Type = var1Value.getClass().getSimpleName(); } } else { throw new IllegalArgumentException("First value should be a variable"); @@ -49,12 +59,16 @@ public boolean test(Map contextVariables) { // Some classes have string values, but are not strings and must be converted first. if (var1Value.getClass().getTypeName().equalsIgnoreCase("ca.uhn.hl7v2.model.v26.datatype.ST") || var1Value.getClass().getTypeName().equalsIgnoreCase("ca.uhn.hl7v2.model.v26.datatype.IS") - || var1Value.getClass().getTypeName().equalsIgnoreCase("ca.uhn.hl7v2.model.v26.datatype.NULLDT")){ + || var1Value.getClass().getTypeName().equalsIgnoreCase("ca.uhn.hl7v2.model.v26.datatype.NULLDT") + + // When we get data back from CustomSegment fields,components or subComponents ... + || var1Value instanceof ca.uhn.hl7v2.model.Varies + || var1Value instanceof ca.uhn.hl7v2.model.GenericPrimitive) { var1Value = Hl7DataHandlerUtil.getStringValue(var1Value); } ConditionPredicateEnum condEnum = ConditionPredicateEnum - .getConditionPredicate(this.conditionOperator, variable1.getIdentifier()); + .getConditionPredicate(this.conditionOperator, var1Type); if (condEnum != null) { // if var2 is a string and must be converted to an integer to test if (var2Value.getClass().getTypeName().equalsIgnoreCase("java.lang.String") @@ -100,6 +114,16 @@ public String getConditionOperator() { return conditionOperator; } - + @Override + public String toString() { + java.io.StringWriter sw = new java.io.StringWriter(); + sw.write(var1.toString()); + sw.write(" "); + sw.write(conditionOperator); + sw.write(" "); + sw.write(var2.toString()); + + return sw.toString(); + } } diff --git a/src/main/java/io/github/linuxforhealth/core/message/AbstractFHIRResourceTemplate.java b/src/main/java/io/github/linuxforhealth/core/message/AbstractFHIRResourceTemplate.java index f46a089c..8388ab75 100644 --- a/src/main/java/io/github/linuxforhealth/core/message/AbstractFHIRResourceTemplate.java +++ b/src/main/java/io/github/linuxforhealth/core/message/AbstractFHIRResourceTemplate.java @@ -17,22 +17,28 @@ public abstract class AbstractFHIRResourceTemplate implements FHIRResourceTempla private boolean repeats; private String resourcePath; private boolean isReferenced; + private boolean ignoreEmpty; + private String condition; @JsonCreator public AbstractFHIRResourceTemplate(@JsonProperty("resourceName") String resourceName, @JsonProperty("resourcePath") String resourcePath, @JsonProperty("isReferenced") boolean isReferenced, - @JsonProperty("repeats") boolean repeats) { + @JsonProperty("repeats") boolean repeats, + @JsonProperty("ignoreEmpty") boolean ignoreEmpty, + @JsonProperty("condition") String conditionExpression) { this.resourceName = resourceName; this.resourcePath = resourcePath; this.repeats = repeats; this.isReferenced = isReferenced; + this.ignoreEmpty = ignoreEmpty; + this.condition = conditionExpression; } public AbstractFHIRResourceTemplate(String resourceName, String resourcePath) { - this(resourceName, resourcePath, false, false); + this(resourceName, resourcePath, false, false, false, null); } @Override @@ -64,7 +70,13 @@ public boolean isReferenced() { return isReferenced; } + @Override + public boolean ignoreEmpty() { + return ignoreEmpty; + } - - + @Override + public String conditionExpression() { + return condition; + } } 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 e8769a5d..fdb2c5b6 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 @@ -33,7 +33,8 @@ private static Specification getHL7Spec(String rawSpec, boolean extractMultiple, if (stk.hasNext()) { String tok = stk.next(); - if (SupportedSegments.contains(tok)) { + // tokens that start with Z are also valid SEGMENTS + if (SupportedSegments.contains(tok) || tok.startsWith("Z")) { segment = tok; if (stk.hasNext()) { field = stk.nextToken(); @@ -41,6 +42,12 @@ private static Specification getHL7Spec(String rawSpec, boolean extractMultiple, if (stk.hasNext()) { component = NumberUtils.toInt(stk.nextToken()); } + + // Don't forget the SubComponents - PID.5.1.2 & PID.5.1.3, can be quite useful + // - allowing us to separate the 'van' and 'van der' prefixes from names like Ludwig van Beethoven, and Cornelius van der Westhuizen + if (stk.hasNext()) { + subComponent = NumberUtils.toInt(stk.nextToken()); + } } else { field = tok; if (stk.hasNext()) { diff --git a/src/main/java/io/github/linuxforhealth/hl7/message/HL7FHIRResourceCondition.java b/src/main/java/io/github/linuxforhealth/hl7/message/HL7FHIRResourceCondition.java new file mode 100644 index 00000000..2342f6a4 --- /dev/null +++ b/src/main/java/io/github/linuxforhealth/hl7/message/HL7FHIRResourceCondition.java @@ -0,0 +1,208 @@ +/* + * (c) Copyright Te Whatu Ora, Health New Zealand, 2023 + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package io.github.linuxforhealth.hl7.message; + +import java.util.ArrayList; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.common.base.Preconditions; + +import ca.uhn.hl7v2.HL7Exception; +import ca.uhn.hl7v2.model.GenericPrimitive; +import ca.uhn.hl7v2.model.Segment; +import ca.uhn.hl7v2.model.Varies; + +import io.github.linuxforhealth.api.EvaluationResult; +import io.github.linuxforhealth.api.InputDataExtractor; +import io.github.linuxforhealth.api.ResourceCondition; +import io.github.linuxforhealth.hl7.expression.specification.HL7Specification; +import io.github.linuxforhealth.hl7.expression.specification.SpecificationParser; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Allows us to conditionally apply a ResourceTemplate depending upon particular values being present in the HL7 message. + * + * EQUALS | NOT_EQUALS | IN | NOT_IN | NULL | NOT_NULL value | [ ... ] + * + * eg: ZSC.2.1.3 NOT_NULL + * ZCR.2(1).1 NULL + * ZCP.3.1 IN [A3, A4, H1, H3, FA, DA] + * ZCP.3.1 NOT_IN [A2, F3, DA] + * ZFD.2 EQUALS A4 + * ZSG NOT_EQUALS H2 + * + * + * Note: Condition cannot be much more complex, as there are no context variables available at evaluation time. + */ +public class HL7FHIRResourceCondition implements ResourceCondition { + public enum Operator { + EQUALS, NOT_EQUALS, IN, NOT_IN, NULL, NOT_NULL + } + + public HL7Specification fieldSpec; // expression for the field to be checked + public Operator op; // Which comparison? + public ArrayList values = new ArrayList(); + + private static final Logger LOGGER = LoggerFactory.getLogger(HL7FHIRResourceCondition.class); + + public HL7FHIRResourceCondition(String condition) { + Preconditions.checkArgument(condition != null && ! condition.isEmpty() && ! condition.isBlank(), + "HL7FHIRResourceCondition (if present) cannot be null, empty or blank"); + + // Chop the condition up into pieces - using a non-trivial regexp + String regexp="([A-Z][A-Z0-9]+(?:\\.\\d+)?(?:\\(\\d\\))?(?:\\.\\d+)?(?:\\.\\d+)?)\\s+(IN|NOT_IN|EQUALS|NOT_EQUALS|NULL|NOT_NULL)(?:\\s+(?:(\\w+)|\\[([\\s\\w,]+)\\])?)?"; + Pattern pattern = Pattern.compile(regexp); + Matcher matcher = pattern.matcher(condition); + + // Make sure the expression matched + if(! matcher.matches()) + throw new IllegalArgumentException(String.format("Can't parse %s - regexp 'fieldSpec EQUALS|NOT_EQUALS|IN|NOT_IN|NULL|NOT_NULL values' doesn't match", condition)); + + // populate our fields + this.fieldSpec = (HL7Specification) SpecificationParser.parse(matcher.group(1), false, false); + op = Operator.valueOf(matcher.group(2)); + + // group(3) and group(4) are both null for NULL and NOT_NULL + if(matcher.group(3) == null && matcher.group(4) == null) { + + // Only on NULL and NOT_NULL + if(! op.equals(Operator.NULL) && ! op.equals(Operator.NOT_NULL)) + throw new IllegalArgumentException(String.format("Can't parse %s - non-value only applies to NULL and NOT_NULL", condition)); + + // ... and we're done + return; + } + + + // group(3) is a single value and only for EQUALS and NOT_EQUALS + if(matcher.group(3) != null) { + + // Only on EQUALS and NOT_EQUALS + if(! op.equals(Operator.EQUALS) && ! op.equals(Operator.NOT_EQUALS)) + throw new IllegalArgumentException(String.format("Can't parse %s - Single value only applies to EQUALS and NOT_EQUALS", condition)); + + values.add(matcher.group(3)); + + // ...and we're done + return; + } + + // We're working with multiple values in group(4) + if(matcher.group(4) == null) { + throw new IllegalArgumentException(String.format("Can't parse %s - Multiple values not found", condition)); + } + + // Only on IN and NOT_IN + if(! op.equals(Operator.IN) && ! op.equals(Operator.NOT_IN)) + throw new IllegalArgumentException(String.format("Can't parse %s - Multiple values only applies to IN and NOT_IN", condition)); + + // group(4) is a single string with multiple values + String toks[] = matcher.group(4).split("\\s*,\\s*"); + + // Make sure we got at least one token + if(toks.length == 0) + throw new IllegalArgumentException(String.format("Can't parse %s - no values for IN | NOT_IN found", condition)); + + for(int ndx=0; ndx additionalSegments; private ResourceModel resource; private List group; - public HL7FHIRResourceTemplateAttributes(Builder builder) { Preconditions.checkArgument(StringUtils.isNotBlank(builder.resourceName), "resourceName cannot be null"); @@ -35,6 +38,9 @@ public HL7FHIRResourceTemplateAttributes(Builder builder) { this.resourcePath = builder.resourcePath; this.repeats = builder.repeats; this.isReferenced = builder.isReferenced; + this.ignoreEmpty = builder.ignoreEmpty; + this.condition = builder.condition; + this.conditionExpression = builder.conditionExpression; additionalSegments = new ArrayList<>(); builder.rawAdditionalSegments .forEach(e -> additionalSegments.add(HL7Segment.parse(e, builder.group))); @@ -85,7 +91,17 @@ public boolean isReferenced() { return isReferenced; } + public boolean ignoreEmpty() { + return ignoreEmpty; + } + + public ResourceCondition condition() { + return condition; + } + public String conditionExpression() { + return conditionExpression; + } private static ResourceModel generateResourceModel(String resourcePath) { return ResourceReader.getInstance().generateResourceModel(resourcePath); @@ -102,6 +118,9 @@ public static class Builder { private String resourcePath; private String group; private boolean isReferenced; + private boolean ignoreEmpty; + private ResourceCondition condition; + private String conditionExpression; private boolean repeats; private ResourceModel resourceModel; @@ -156,6 +175,17 @@ public Builder withIsReferenced(boolean isReferenced) { return this; } + public Builder withignoreEmpty(boolean ignoreEmpty) { + this.ignoreEmpty = ignoreEmpty; + return this; + } + + public Builder withCondition(String conditionExpr) { + this.conditionExpression = conditionExpr; + this.condition = new HL7FHIRResourceCondition(conditionExpr); + return this; + } + public Builder withResourceModel(ResourceModel resourceModel) { this.resourceModel = resourceModel; return this; diff --git a/src/main/java/io/github/linuxforhealth/hl7/message/HL7MessageEngine.java b/src/main/java/io/github/linuxforhealth/hl7/message/HL7MessageEngine.java index f83e1bcd..aacebfd5 100644 --- a/src/main/java/io/github/linuxforhealth/hl7/message/HL7MessageEngine.java +++ b/src/main/java/io/github/linuxforhealth/hl7/message/HL7MessageEngine.java @@ -27,11 +27,14 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +import ca.uhn.hl7v2.HL7Exception; import ca.uhn.hl7v2.model.Structure; +import ca.uhn.hl7v2.model.Visitable; import io.github.linuxforhealth.api.EvaluationResult; import io.github.linuxforhealth.api.FHIRResourceTemplate; import io.github.linuxforhealth.api.InputDataExtractor; import io.github.linuxforhealth.api.MessageEngine; +import io.github.linuxforhealth.api.ResourceCondition; import io.github.linuxforhealth.api.ResourceModel; import io.github.linuxforhealth.api.ResourceValue; import io.github.linuxforhealth.core.Constants; @@ -189,7 +192,7 @@ private List generateResources(HL7MessageData hl7DataInput, if (!multipleSegments.isEmpty()) { resourceResults = generateMultipleResources(hl7DataInput, resourceModel, contextValues, - multipleSegments, template.isGenerateMultiple()); + multipleSegments, template.isGenerateMultiple(), template.ignoreEmpty(), template.condition()); } return resourceResults; } @@ -284,7 +287,8 @@ private static List getMultipleSegments(final HL7MessageData hl7Da private static List generateMultipleResources(final HL7MessageData hl7DataInput, final ResourceModel rs, final Map contextValues, - final List multipleSegments, boolean generateMultiple) { + final List multipleSegments, boolean generateMultiple, boolean ignoreEmpty, + ResourceCondition condition) { List resourceResults = new ArrayList<>(); for (SegmentGroup currentGroup : multipleSegments) { @@ -300,16 +304,29 @@ private static List generateMultipleResources(final HL7MessageDa for (EvaluationResult baseValue : baseValues) { try { - ResourceResult result = rs.evaluate(hl7DataInput, ImmutableMap.copyOf(localContextValues), - baseValue); - if (result != null && result.getValue() != null) { - resourceResults.add(result); - if (!generateMultiple) { - // If only single resource needs to be generated then return. - return resourceResults; + // We need to check if the baseValue is empty + Visitable vs = baseValue.getValue(); + if((vs != null && ! vs.isEmpty()) || ! ignoreEmpty) { + + // Ok the baseValue is either not empty or we're not allowed to ignore empty segments + // (We can't rely on empty segment giving us an empty resource; as common templates populate Resource.meta fields) + + // If we have a condition then make sure it is satisfied before we translate this baseValue into a Resource + if (condition == null || condition.isConditionSatisfied(hl7DataInput, baseValue)) { + + ResourceResult result = rs.evaluate(hl7DataInput, ImmutableMap.copyOf(localContextValues), + baseValue); + + if (result != null && result.getValue() != null) { + resourceResults.add(result); + if (!generateMultiple) { + // If only single resource needs to be generated then return. + return resourceResults; + } + } } } - } catch (RequiredConstraintFailureException | IllegalArgumentException + } catch (RequiredConstraintFailureException | IllegalArgumentException | HL7Exception | IllegalStateException e) { LOGGER.warn("generateMultipleResources - Exception encountered"); LOGGER.debug("generateMultipleResources - Exception encountered", e); diff --git a/src/test/java/io/github/linuxforhealth/hl7/expression/Hl7ExpressionTest.java b/src/test/java/io/github/linuxforhealth/hl7/expression/Hl7ExpressionTest.java index 5b3bdefb..f18fa8f2 100644 --- a/src/test/java/io/github/linuxforhealth/hl7/expression/Hl7ExpressionTest.java +++ b/src/test/java/io/github/linuxforhealth/hl7/expression/Hl7ExpressionTest.java @@ -18,6 +18,8 @@ import ca.uhn.hl7v2.model.Type; import io.github.linuxforhealth.api.EvaluationResult; import io.github.linuxforhealth.core.expression.SimpleEvaluationResult; +import io.github.linuxforhealth.core.expression.condition.ConditionUtil; +import io.github.linuxforhealth.core.expression.condition.SimpleBiCondition; import io.github.linuxforhealth.core.terminology.SimpleCode; import io.github.linuxforhealth.hl7.message.HL7MessageData; import io.github.linuxforhealth.hl7.parsing.HL7DataExtractor; @@ -123,8 +125,10 @@ void test3_field() throws IOException { } + + // PID.3.1 is a COMPONENT @Test - void test3_field_subcomponent() throws IOException { + void test3_field_component() throws IOException { String message = "MSH|^~\\&|hl7Integration|hl7Integration|||||ADT^A01|||2.3|\r" + "EVN|A01|20130617154644\r" + "PID|1|465 306 5961|000010016^^^MR~000010017^^^MR~000010018^^^MR|407623|Wood^Patrick^^^MR||19700101|female|||High Street^^Oxford^^Ox1 4DP~George St^^Oxford^^Ox1 5AP|||||||\r" @@ -158,7 +162,80 @@ void test3_field_subcomponent() throws IOException { } + // PID.5.1.3 is a SUBCOMPONENT + @Test + void test3_field_subcomponent() throws IOException { + String message = "MSH|^~\\&|hl7Integration|hl7Integration|||||ADT^A01|||2.3|\r" + + "EVN|A01|20130617154644\r" + + "PID|1|465 306 5961|000010016^^^MR~000010017^^^MR~000010018^^^MR|407623|&van der&Westhuizen^Cornelius^^^MR||19700101|female|||High Street^^Oxford^^Ox1 4DP~George St^^Oxford^^Ox1 5AP|||||||\r" + + "NK1|1|Wood^John^^^MR|Father||999-9999\r" + "NK1|2|Jones^Georgie^^^MSS|MOTHER||999-9999\r" + + "PV1|1||Location||||||||||||||||261938_6_201306171546|||||||||||||||||||||||||20130617134644|||||||||"; + + Message hl7message = getMessage(message); + + HL7DataExtractor hl7DTE = new HL7DataExtractor(hl7message); + + + Structure s = hl7DTE.getStructure("PID", 0).getValue(); + + + ExpressionAttributes attr = + new ExpressionAttributes.Builder().withSpecs("PID.5.1.3").withType("String").build(); + Hl7Expression exp = new Hl7Expression(attr); + + + Map context = new HashMap<>(); + // context.put("PID", new SimpleEvaluationResult(s)); + + + EvaluationResult value = exp.evaluate(new HL7MessageData(hl7DTE), ImmutableMap.copyOf(context), + new SimpleEvaluationResult(s)); + + + assertThat((String) value.getValue()).isEqualTo("Westhuizen"); + } + // ZAL.2.1 is a COMPONENT in a custom SEGMENT + @Test + void test3_field_customcomponent() throws IOException { + String message = "MSH|^~\\&|hl7Integration|hl7Integration|||||ADT^A01|||2.3|\r" + + "EVN|A01|20130617154644\r" + + "PID|1|465 306 5961|000010016^^^MR~000010017^^^MR~000010018^^^MR|407623|Wood^Patrick^^^MR||19700101|female|||High Street^^Oxford^^Ox1 4DP~George St^^Oxford^^Ox1 5AP|||||||\r" + + "NK1|1|Wood^John^^^MR|Father||999-9999\r" + "NK1|2|Jones^Georgie^^^MSS|MOTHER||999-9999\r" + + "PV1|1||Location||||||||||||||||261938_6_201306171546|||||||||||||||||||||||||20130617134644|||||||||\r" + + "ZAL|1|H3^Other allergies^webPAS|L01^Latex^webPAS|"; + + + Message hl7message = getMessage(message); + + HL7DataExtractor hl7DTE = new HL7DataExtractor(hl7message); + + + Structure s = hl7DTE.getStructure("ZAL", 0).getValue(); + + + ExpressionAttributes attr = + new ExpressionAttributes.Builder().withSpecs("ZAL.2.1").withType("String").build(); + Hl7Expression exp = new Hl7Expression(attr); + + + Map context = new HashMap<>(); + // context.put("PID", new SimpleEvaluationResult(s)); + + + EvaluationResult value = exp.evaluate(new HL7MessageData(hl7DTE), ImmutableMap.copyOf(context), + new SimpleEvaluationResult(s)); + + + assertThat((String) value.getValue()).isEqualTo("H3"); + + // Can we stick it into a variable and make it work in a Condition ? + SimpleBiCondition simplecondition = (SimpleBiCondition) ConditionUtil.createCondition("$zal21 EQUALS H3"); + Map contextVariables = new HashMap<>(); + contextVariables.put("zal21", value); + + assertThat(simplecondition.test(contextVariables)).isTrue(); +} @Test void test3_field_options() throws IOException { diff --git a/src/test/java/io/github/linuxforhealth/hl7/message/Hl7ConditionalMappingTest.java b/src/test/java/io/github/linuxforhealth/hl7/message/Hl7ConditionalMappingTest.java new file mode 100644 index 00000000..a0a0662e --- /dev/null +++ b/src/test/java/io/github/linuxforhealth/hl7/message/Hl7ConditionalMappingTest.java @@ -0,0 +1,327 @@ +/* + * (c) Te Whatu Ora, Health New Zealand, 2023 + * + * SPDX-License-Identifier: Apache-2.0 + * + * @author Stuart McGrigor + */ + +package io.github.linuxforhealth.hl7.message; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Properties; + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.AllergyIntolerance; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.ResourceType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.provider.CsvSource; + +import io.github.linuxforhealth.core.config.ConverterConfiguration; +import io.github.linuxforhealth.fhir.FHIRContext; +import io.github.linuxforhealth.hl7.ConverterOptions; +import io.github.linuxforhealth.hl7.ConverterOptions.Builder; +import io.github.linuxforhealth.hl7.HL7ToFHIRConverter; +import io.github.linuxforhealth.hl7.resource.ResourceReader; +import io.github.linuxforhealth.hl7.segments.util.ResourceUtils; + +// This class uses the ability to create ADDITIONAL HL7 messages to convert weird HL7 messages +// that exercise the new conditional Resource Template functionality +// +// In these tests, the additional message definitions for (entirely ficticious) ADT^A11 messages +// are placed in src/test/resources/additional_resources/hl7/message/ADT_A11.yml + +class Hl7ConditionalMappingTest { + + // # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + // NOTE VALIDATION IS INTENTIONALLY NOT USED BECAUSE WE ARE CREATING RESOURCES THAT ARE NOT STANDARD + private static final ConverterOptions OPTIONS = new Builder().withPrettyPrint().build(); + // # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + private static final String CONF_PROP_HOME = "hl7converter.config.home"; + + @TempDir + static File folder; + + static String originalConfigHome; + + @BeforeAll + static void saveConfigHomeProperty() { + originalConfigHome = System.getProperty(CONF_PROP_HOME); + ConverterConfiguration.reset(); + ResourceReader.reset(); + folder.setWritable(true); + } + + @AfterEach + void reset() { + System.clearProperty(CONF_PROP_HOME); + ConverterConfiguration.reset(); + ResourceReader.reset(); + } + + @AfterAll + static void reloadPreviousConfigurations() { + if (originalConfigHome != null) + System.setProperty(CONF_PROP_HOME, originalConfigHome); + else + System.clearProperty(CONF_PROP_HOME); + folder.setWritable(true); + } + + + // Conditional Segment test - Custom SEGMENT value -- ZSG + @ParameterizedTest + @CsvSource({ "ADT^A11,ZSG|L2\r,1", + "ADT^A11,ZSG|A3\r,0", + "ADT^A11,ZSG|L2|Two\r,1", // L2 is a FIELD value, but SEGMENT comparison will return first field + "ADT^A11,ZSG|L2^Lookup List 2^WebPAS\r,0" // L2 is a COMPONENT value so SEGMENT comparison will fail + }) + void testConditionalSegmentValueIsSegment(String messageType, String zSegment, int aiCount) throws IOException { + + // Set up the config file + commonConfigFileSetup(); + + // An empty AL1 Segment... + String hl7message = "MSH|^~\\&|TestSystem||TestTransformationAgent||20150502090000||" + messageType + "|controlID|P|2.6\r" + + "EVN|A01|20150502090000|\r" + + "PID|||1234^^^^MR||DOE^JANE^|||F||||||||||||||||||||||\r" + + "PV1||I||||||||SUR||||||||S|VisitNumber^^^ACME|A||||||||||||||||||||||||20150502090000|\r" + + zSegment; + + List e = getBundleEntryFromHL7Message(hl7message); + + List patientResource = ResourceUtils.getResourceList(e, ResourceType.Patient); + assertThat(patientResource).hasSize(1); // from PID + + List encounterResource = ResourceUtils.getResourceList(e, ResourceType.Encounter); + assertThat(encounterResource).hasSize(1); // from EVN, PV1 + + List allergyIntoleranceResource = ResourceUtils.getResourceList(e, ResourceType.AllergyIntolerance); + assertThat(allergyIntoleranceResource).hasSize(aiCount); // How many AllergyIntolerance segments ? + + // Confirm that there are no extra resources + assertThat(e).hasSize(2 + aiCount); + } + + // Conditional Segment test - Custom FIELD Value - ZFD-2 + @ParameterizedTest + @CsvSource({ "ADT^A11,ZFD|1|L2\r,1", + "ADT^A11,ZFD|1|A3\r,0", + "ADT^A11,ZFD|1|L2|Three|Four\r,1", + "ADT^A11,ZFD|L2\r,0", // L2 is a SEGMENT value so FIELD comparison will fail + "ADT^A11,ZFD|1|L2^Lookup List 2^WebPAS\r,0" // L2 is a COMPONENT value so FIELD comparison will fail + }) + void testConditionalSegmentValueIsField(String messageType, String zSegment, int aiCount) throws IOException { + + // Set up the config file + commonConfigFileSetup(); + + // An empty AL1 Segment... + String hl7message = "MSH|^~\\&|TestSystem||TestTransformationAgent||20150502090000||" + messageType + "|controlID|P|2.6\r" + + "EVN|A01|20150502090000|\r" + + "PID|||1234^^^^MR||DOE^JANE^|||F||||||||||||||||||||||\r" + + "PV1||I||||||||SUR||||||||S|VisitNumber^^^ACME|A||||||||||||||||||||||||20150502090000|\r" + + zSegment; + + List e = getBundleEntryFromHL7Message(hl7message); + + List patientResource = ResourceUtils.getResourceList(e, ResourceType.Patient); + assertThat(patientResource).hasSize(1); // from PID + + List encounterResource = ResourceUtils.getResourceList(e, ResourceType.Encounter); + assertThat(encounterResource).hasSize(1); // from EVN, PV1 + + List allergyIntoleranceResource = ResourceUtils.getResourceList(e, ResourceType.AllergyIntolerance); + assertThat(allergyIntoleranceResource).hasSize(aiCount); // How many AllergyIntolerance segments ? + + // Confirm that there are no extra resources + assertThat(e).hasSize(2 + aiCount); + } + + // Conditional Segment test - Custom COMPONENT Value - ZCP.2.1 + @ParameterizedTest + @CsvSource({ "ADT^A11,ZCP|1|A3^Allergies^WebPAS\r,1", + "ADT^A11,ZCP|1|H1^Drug Reactions^WebPAS\r,1", + "ADT^A11,ZCP|1|H3^Other Allergies^WebPAS\r,1", + "ADT^A11,ZCP|1|H2^Health Conditions^WebPAS\r,0", // H2 not IN set + "ADT^A11,ZCP|1|H4^Infection Prevention Alerts^WebPAS\r,0", // H4 not IN set + "ADT^A11,ZCP|1|H3|three|four\r,1" // H3 in FIELD, yet COMPONENT comparison succeeds + }) + void testConditionalSegmentValueIsComponent(String messageType, String zSegment, int aiCount) throws IOException { + + // Set up the config file + commonConfigFileSetup(); + + // An empty AL1 Segment... + String hl7message = "MSH|^~\\&|TestSystem||TestTransformationAgent||20150502090000||" + messageType + "|controlID|P|2.6\r" + + "EVN|A01|20150502090000|\r" + + "PID|||1234^^^^MR||DOE^JANE^|||F||||||||||||||||||||||\r" + + "PV1||I||||||||SUR||||||||S|VisitNumber^^^ACME|A||||||||||||||||||||||||20150502090000|\r" + + zSegment; + + List e = getBundleEntryFromHL7Message(hl7message); + + List patientResource = ResourceUtils.getResourceList(e, ResourceType.Patient); + assertThat(patientResource).hasSize(1); // from PID + + List encounterResource = ResourceUtils.getResourceList(e, ResourceType.Encounter); + assertThat(encounterResource).hasSize(1); // from EVN, PV1 + + List allergyIntoleranceResource = ResourceUtils.getResourceList(e, ResourceType.AllergyIntolerance); + assertThat(allergyIntoleranceResource).hasSize(aiCount); // How many AllergyIntolerance segments ? + + // Confirm that there are no extra resources + assertThat(e).hasSize(2 + aiCount); + } + + // Conditional Segment test - Custom SUB COMPONENT Value - ZSC-2.1.2 + @ParameterizedTest + @CsvSource({ "ADT^A11,ZSC|1|pre&A3^Allergies^WebPAS\r,1", + "ADT^A11,ZSC|1|pre&H1^Drug Reactions^WebPAS\r,1", + "ADT^A11,ZSC|1|pre&H3^Other Allergies^WebPAS\r,1", + "ADT^A11,ZSC|1|pre&H2^Health Conditions^WebPAS\r,0", // H2 not IN set + "ADT^A11,ZSC|1|pre&H4^Infection Prevention Alerts^WebPAS\r,0", // H4 not IN set + "ADT^A11,ZSC|1|H3|three|four\r,0", // H3 in FIELD so SUB COMPONENT comparison will fail + "ADT^A11,ZSC|1|H3^Other Allergies^WebPAS|three|four\r,0" // H3 in COMPONENT, so SUB COMPONENT comparison will fail + }) + void testConditionalSegmentValueIsSubComponent(String messageType, String zSegment, int aiCount) throws IOException { + + // Set up the config file + commonConfigFileSetup(); + + // An empty AL1 Segment... + String hl7message = "MSH|^~\\&|TestSystem||TestTransformationAgent||20150502090000||" + messageType + "|controlID|P|2.6\r" + + "EVN|A01|20150502090000|\r" + + "PID|||1234^^^^MR||DOE^JANE^|||F||||||||||||||||||||||\r" + + "PV1||I||||||||SUR||||||||S|VisitNumber^^^ACME|A||||||||||||||||||||||||20150502090000|\r" + + zSegment; + + List e = getBundleEntryFromHL7Message(hl7message); + + List patientResource = ResourceUtils.getResourceList(e, ResourceType.Patient); + assertThat(patientResource).hasSize(1); // from PID + + List encounterResource = ResourceUtils.getResourceList(e, ResourceType.Encounter); + assertThat(encounterResource).hasSize(1); // from EVN, PV1 + + List allergyIntoleranceResource = ResourceUtils.getResourceList(e, ResourceType.AllergyIntolerance); + assertThat(allergyIntoleranceResource).hasSize(aiCount); // How many AllergyIntolerance segments ? + + // Confirm that there are no extra resources + assertThat(e).hasSize(2 + aiCount); + } + + + // Conditional Segment test - repetition - ZCR.2(1).1 -- repetitions start at Zero + @ParameterizedTest + @CsvSource({ "ADT^A11,ZCP|1|A3^Allergies^WebPAS~H2^Other Allergies^WebPAS\r,0", + "ADT^A11,ZCP|1|H1^Drug Reactions^WebPAS~A3^Allergies^WebPAS\r,1" }) + void testConditionalSegmentRepetitions(String messageType, String zSegment, int aiCount) throws IOException { + + // Set up the config file + commonConfigFileSetup(); + + // An empty AL1 Segment... + String hl7message = "MSH|^~\\&|TestSystem||TestTransformationAgent||20150502090000||" + messageType + "|controlID|P|2.6\r" + + "EVN|A01|20150502090000|\r" + + "PID|||1234^^^^MR||DOE^JANE^|||F||||||||||||||||||||||\r" + + "PV1||I||||||||SUR||||||||S|VisitNumber^^^ACME|A||||||||||||||||||||||||20150502090000|\r" + + "ZCP|1|A3^Allergies^WebPAS\r" // included + + "ZCP|2|H1^Drug Reactions^WebPAS\r" // included + + "ZCP|3|H2^Health Conditions^WebPAS\r" // dropped + + "ZCP|4|H3^Other Allergies^WebPAS\r" // included + + "ZCP|5|H4^Infection Prevention Alerts^WebPAS\r"; // dropped + + List e = getBundleEntryFromHL7Message(hl7message); + + List patientResource = ResourceUtils.getResourceList(e, ResourceType.Patient); + assertThat(patientResource).hasSize(1); // from PID + + List encounterResource = ResourceUtils.getResourceList(e, ResourceType.Encounter); + assertThat(encounterResource).hasSize(1); // from EVN, PV1 + + List allergyIntoleranceResource = ResourceUtils.getResourceList(e, ResourceType.AllergyIntolerance); + assertThat(allergyIntoleranceResource).hasSize(3); // How many AllergyIntolerance segments ? + + // Confirm that there are no extra resources + assertThat(e).hasSize(5); + } + + // Conditional Segment test - multiple segments - make sure we don't get confusion in which segment we're testing + @ParameterizedTest + @CsvSource({ "ADT^A11,ZCP|1|A3^Allergies^WebPAS\r,1", + "ADT^A11,ZCP|1|H1^Drug Reactions^WebPAS\r,1", + "ADT^A11,ZCP|1|H2^Health Conditions^WebPAS\r,0", + "ADT^A11,ZCP|1|H3^Other Allergies^WebPAS\r,1", + "ADT^A11,ZCP|1|H4^Infection Prevention Alerts^WebPAS\r,0"}) + void testConditionalSegmentMultiples(String messageType, String zSegment, int aiCount) throws IOException { + + // Set up the config file + commonConfigFileSetup(); + + // An empty AL1 Segment... + String hl7message = "MSH|^~\\&|TestSystem||TestTransformationAgent||20150502090000||" + messageType + "|controlID|P|2.6\r" + + "EVN|A01|20150502090000|\r" + + "PID|||1234^^^^MR||DOE^JANE^|||F||||||||||||||||||||||\r" + + "PV1||I||||||||SUR||||||||S|VisitNumber^^^ACME|A||||||||||||||||||||||||20150502090000|\r" + + "ZCP|1|A3^Allergies^WebPAS\r" // included + + "ZCP|2|H1^Drug Reactions^WebPAS\r" // included + + "ZCP|3|H2^Health Conditions^WebPAS\r" // dropped + + "ZCP|4|H3^Other Allergies^WebPAS\r" // included + + "ZCP|5|H4^Infection Prevention Alerts^WebPAS\r"; // dropped + + List e = getBundleEntryFromHL7Message(hl7message); + + List patientResource = ResourceUtils.getResourceList(e, ResourceType.Patient); + assertThat(patientResource).hasSize(1); // from PID + + List encounterResource = ResourceUtils.getResourceList(e, ResourceType.Encounter); + assertThat(encounterResource).hasSize(1); // from EVN, PV1 + + List allergyIntoleranceResource = ResourceUtils.getResourceList(e, ResourceType.AllergyIntolerance); + assertThat(allergyIntoleranceResource).hasSize(3); // How many AllergyIntolerance segments ? + + // Confirm that there are no extra resources + assertThat(e).hasSize(5); + } + + private static void commonConfigFileSetup() throws IOException { + File configFile = new File(folder, "config.properties"); + Properties prop = new Properties(); + prop.put("base.path.resource", "src/main/resources"); + prop.put("supported.hl7.messages", "ADT_A11"); // Must need to define our weird ADT message + prop.put("default.zoneid", "+08:00"); + // Location of custom (or merely additional) resources + prop.put("additional.resources.location", "src/test/resources/additional_resources"); + prop.store(new FileOutputStream(configFile), null); + System.setProperty(CONF_PROP_HOME, configFile.getParent()); + } + + // Need custom convert sequence with options that turn off FHIR validation. + private static List getBundleEntryFromHL7Message(String hl7message) { + HL7ToFHIRConverter ftv = new HL7ToFHIRConverter(); // Testing loading of config which happens once per instantiation + String json = ftv.convert(hl7message, OPTIONS); // Need custom options that turn off FHIR validation. + assertThat(json).isNotNull(); + FHIRContext context = new FHIRContext(); + IBaseResource bundleResource = context.getParser().parseResource(json); + assertThat(bundleResource).isNotNull(); + Bundle b = (Bundle) bundleResource; + return b.getEntry(); + } + +} diff --git a/src/test/java/io/github/linuxforhealth/hl7/message/Hl7CustomMessageTest.java b/src/test/java/io/github/linuxforhealth/hl7/message/Hl7CustomMessageTest.java index 6b8b2c0e..2d36d9c8 100644 --- a/src/test/java/io/github/linuxforhealth/hl7/message/Hl7CustomMessageTest.java +++ b/src/test/java/io/github/linuxforhealth/hl7/message/Hl7CustomMessageTest.java @@ -81,7 +81,6 @@ static void reloadPreviousConfigurations() { @Test void testCustomPatMessage() throws IOException { - // Set up the config file commonConfigFileSetup(); String hl7message = "MSH|^~\\&|||||20211005105125||CUSTOM^PAT|1a3952f1-38fe-4d55-95c6-ce58ebfc7f10|P|2.6\n" @@ -113,7 +112,8 @@ private static void commonConfigFileSetup() throws IOException { prop.put("base.path.resource", "src/main/resources"); prop.put("supported.hl7.messages", "*"); // Must use wild card so the custom resources are found. prop.put("default.zoneid", "+08:00"); - prop.put("additional.resources.location", "src/test/resources/additional_custom_resources"); // Location of custom resources + // Location of custom (or merely additional) resources + prop.put("additional.resources.location", "src/test/resources/additional_custom_resources"); prop.store(new FileOutputStream(configFile), null); System.setProperty(CONF_PROP_HOME, configFile.getParent()); } diff --git a/src/test/java/io/github/linuxforhealth/hl7/message/Hl7CustomSegmentConditionTest.java b/src/test/java/io/github/linuxforhealth/hl7/message/Hl7CustomSegmentConditionTest.java new file mode 100644 index 00000000..e76e9f65 --- /dev/null +++ b/src/test/java/io/github/linuxforhealth/hl7/message/Hl7CustomSegmentConditionTest.java @@ -0,0 +1,241 @@ +/* + * (c) Te Whatu Ora, Health New Zealand, 2023 + * + * SPDX-License-Identifier: Apache-2.0 + * + * @author Stuart McGrigor + */ + +package io.github.linuxforhealth.hl7.message; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Properties; + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.AllergyIntolerance; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.ResourceType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import io.github.linuxforhealth.core.config.ConverterConfiguration; +import io.github.linuxforhealth.fhir.FHIRContext; +import io.github.linuxforhealth.hl7.ConverterOptions; +import io.github.linuxforhealth.hl7.ConverterOptions.Builder; +import io.github.linuxforhealth.hl7.HL7ToFHIRConverter; +import io.github.linuxforhealth.hl7.resource.ResourceReader; +import io.github.linuxforhealth.hl7.segments.util.ResourceUtils; + +// This class uses the ability to create ADDITIONAL HL7 messages to convert weird HL7 messages +// that exercise the new conditional Resource Template functionality +// +// In these tests, the additional message definitions for (entirely ficticious) ADT^A11 messages +// are placed in src/test/resources/additional_resources/hl7/message/ADT_A11.yml +// +// ... and the mappings are placed in src/test/resources/hl7/resource/AllergyIntoleranceZ*.yml +// +// We are verifying that we can extract FIELD, COMPONENT and SUB COMPONENT values from custom segments and use those +// values in conditions. + +class Hl7CustomSegmentConditionTest { + + // # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + // NOTE VALIDATION IS INTENTIONALLY NOT USED BECAUSE WE ARE CREATING RESOURCES THAT ARE NOT STANDARD + private static final ConverterOptions OPTIONS = new Builder().withPrettyPrint().build(); + // # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + private static final String CONF_PROP_HOME = "hl7converter.config.home"; + + @TempDir + static File folder; + + static String originalConfigHome; + + @BeforeAll + static void saveConfigHomeProperty() { + originalConfigHome = System.getProperty(CONF_PROP_HOME); + ConverterConfiguration.reset(); + ResourceReader.reset(); + folder.setWritable(true); + } + + @AfterEach + void reset() { + System.clearProperty(CONF_PROP_HOME); + ConverterConfiguration.reset(); + ResourceReader.reset(); + } + + @AfterAll + static void reloadPreviousConfigurations() { + if (originalConfigHome != null) + System.setProperty(CONF_PROP_HOME, originalConfigHome); + else + System.clearProperty(CONF_PROP_HOME); + folder.setWritable(true); + } + + + // Custom Segment Field Condition Test ZFD + @ParameterizedTest + @CsvSource({ "ADT^A11,ZFD|1|L2|H1|four\r,ALLERGY", // ZFD.3 = H1 --> allergy + "ADT^A11,ZFD|1|L2|H2|four\r,ALLERGY", // ZFD.3 = H2 --> allergy + "ADT^A11,ZFD|1|L2|H3|four\r,INTOLERANCE", // ZFD.3 = H3 --> intolerance + }) + void testCustomSegmentFieldInCondition(String messageType, String zSegment, String aiType) throws IOException { + + // Set up the config file + commonConfigFileSetup(); + + // An empty AL1 Segment... + String hl7message = "MSH|^~\\&|TestSystem||TestTransformationAgent||20150502090000||" + messageType + "|controlID|P|2.6\r" + + "EVN|A01|20150502090000|\r" + + "PID|||1234^^^^MR||DOE^JANE^|||F||||||||||||||||||||||\r" + + "PV1||I||||||||SUR||||||||S|VisitNumber^^^ACME|A||||||||||||||||||||||||20150502090000|\r" + + zSegment; + + List e = getBundleEntryFromHL7Message(hl7message); + + List patientResource = ResourceUtils.getResourceList(e, ResourceType.Patient); + assertThat(patientResource).hasSize(1); // from PID + + List encounterResource = ResourceUtils.getResourceList(e, ResourceType.Encounter); + assertThat(encounterResource).hasSize(1); // from EVN, PV1 + + List allergyIntoleranceResources = ResourceUtils.getResourceList(e, ResourceType.AllergyIntolerance); + assertThat(allergyIntoleranceResources).hasSize(1) // from ZFD segment + .element(0).satisfies(ai -> { + + // Make sure the type field is present & correct + assertThat(((AllergyIntolerance) ai).hasType()).isTrue(); + assertThat(((AllergyIntolerance) ai).getType()).hasToString(aiType); + }); + + + + // Confirm that there are no extra resources + assertThat(e).hasSize(3); + } + + + // Custom Segment COMPONENT Condition Test ZCP + @ParameterizedTest + @CsvSource({ "ADT^A11,ZCP|1|A3^yy|xx^H1|four\r,ALLERGY", // ZCP.3.2 = H1 --> allergy + "ADT^A11,ZCP|1|A3^yy|xx^H2|four\r,ALLERGY", // ZCP.3.2 = H2 --> allergy + "ADT^A11,ZCP|1|A3^yy|xx^H3|four\r,INTOLERANCE", // ZCP.3.2 = H3 --> intolerance + }) + void testCustomSegmentComponentInCondition(String messageType, String zSegment, String aiType) throws IOException { + + // Set up the config file + commonConfigFileSetup(); + + // An empty AL1 Segment... + String hl7message = "MSH|^~\\&|TestSystem||TestTransformationAgent||20150502090000||" + messageType + "|controlID|P|2.6\r" + + "EVN|A01|20150502090000|\r" + + "PID|||1234^^^^MR||DOE^JANE^|||F||||||||||||||||||||||\r" + + "PV1||I||||||||SUR||||||||S|VisitNumber^^^ACME|A||||||||||||||||||||||||20150502090000|\r" + + zSegment; + + List e = getBundleEntryFromHL7Message(hl7message); + + List patientResource = ResourceUtils.getResourceList(e, ResourceType.Patient); + assertThat(patientResource).hasSize(1); // from PID + + List encounterResource = ResourceUtils.getResourceList(e, ResourceType.Encounter); + assertThat(encounterResource).hasSize(1); // from EVN, PV1 + + List allergyIntoleranceResources = ResourceUtils.getResourceList(e, ResourceType.AllergyIntolerance); + assertThat(allergyIntoleranceResources).hasSize(1) // from ZFD segment + .element(0).satisfies(ai -> { + + // Make sure the type field is present & correct + assertThat(((AllergyIntolerance) ai).hasType()).isTrue(); + assertThat(((AllergyIntolerance) ai).getType()).hasToString(aiType); + }); + + + + // Confirm that there are no extra resources + assertThat(e).hasSize(3); + } + + + // Custom Segment SUBCOMPONENT Condition Test ZCP + @ParameterizedTest + @CsvSource({ "ADT^A11,ZSC|1|zz&A3^yy|xx^ww&H1|four\r,ALLERGY", // ZCP.3.2.2 = H1 --> allergy + "ADT^A11,ZSC|1|zz&A3^yy|xx^ww&H2|four\r,ALLERGY", // ZCP.3.2.2 = H2 --> allergy + "ADT^A11,ZSC|1|zz&A3^yy|xx^ww&H3|four\r,INTOLERANCE", // ZCP.3.2.2 = H3 --> intolerance + }) + void testCustomSegmentSubComponentInCondition(String messageType, String zSegment, String aiType) throws IOException { + + // Set up the config file + commonConfigFileSetup(); + + // An empty AL1 Segment... + String hl7message = "MSH|^~\\&|TestSystem||TestTransformationAgent||20150502090000||" + messageType + "|controlID|P|2.6\r" + + "EVN|A01|20150502090000|\r" + + "PID|||1234^^^^MR||DOE^JANE^|||F||||||||||||||||||||||\r" + + "PV1||I||||||||SUR||||||||S|VisitNumber^^^ACME|A||||||||||||||||||||||||20150502090000|\r" + + zSegment; + + List e = getBundleEntryFromHL7Message(hl7message); + + List patientResource = ResourceUtils.getResourceList(e, ResourceType.Patient); + assertThat(patientResource).hasSize(1); // from PID + + List encounterResource = ResourceUtils.getResourceList(e, ResourceType.Encounter); + assertThat(encounterResource).hasSize(1); // from EVN, PV1 + + List allergyIntoleranceResources = ResourceUtils.getResourceList(e, ResourceType.AllergyIntolerance); + assertThat(allergyIntoleranceResources).hasSize(1) // from ZFD segment + .element(0).satisfies(ai -> { + + // Make sure the type field is present & correct + assertThat(((AllergyIntolerance) ai).hasType()).isTrue(); + assertThat(((AllergyIntolerance) ai).getType()).hasToString(aiType); + }); + + + + // Confirm that there are no extra resources + assertThat(e).hasSize(3); + } + + + + private static void commonConfigFileSetup() throws IOException { + File configFile = new File(folder, "config.properties"); + Properties prop = new Properties(); + prop.put("base.path.resource", "src/main/resources"); + prop.put("supported.hl7.messages", "ADT_A11"); // Must need to define our weird ADT message + prop.put("default.zoneid", "+08:00"); + // Location of custom (or merely additional) resources + prop.put("additional.resources.location", "src/test/resources/additional_resources"); + prop.store(new FileOutputStream(configFile), null); + System.setProperty(CONF_PROP_HOME, configFile.getParent()); + } + + // Need custom convert sequence with options that turn off FHIR validation. + private static List getBundleEntryFromHL7Message(String hl7message) { + HL7ToFHIRConverter ftv = new HL7ToFHIRConverter(); // Testing loading of config which happens once per instantiation + String json = ftv.convert(hl7message, OPTIONS); // Need custom options that turn off FHIR validation. + assertThat(json).isNotNull(); + FHIRContext context = new FHIRContext(); + IBaseResource bundleResource = context.getParser().parseResource(json); + assertThat(bundleResource).isNotNull(); + Bundle b = (Bundle) bundleResource; + return b.getEntry(); + } + +} diff --git a/src/test/java/io/github/linuxforhealth/hl7/message/Hl7IgnoreEmptyTest.java b/src/test/java/io/github/linuxforhealth/hl7/message/Hl7IgnoreEmptyTest.java new file mode 100644 index 00000000..52bea791 --- /dev/null +++ b/src/test/java/io/github/linuxforhealth/hl7/message/Hl7IgnoreEmptyTest.java @@ -0,0 +1,157 @@ +/* + * (c) Te Whatu Ora, Health New Zealand, 2023 + * + * SPDX-License-Identifier: Apache-2.0 + * + * @author Stuart McGrigor + */ + +package io.github.linuxforhealth.hl7.message; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Properties; + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.AllergyIntolerance; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.ResourceType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import io.github.linuxforhealth.core.config.ConverterConfiguration; +import io.github.linuxforhealth.fhir.FHIRContext; +import io.github.linuxforhealth.hl7.ConverterOptions; +import io.github.linuxforhealth.hl7.ConverterOptions.Builder; +import io.github.linuxforhealth.hl7.HL7ToFHIRConverter; +import io.github.linuxforhealth.hl7.resource.ResourceReader; +import io.github.linuxforhealth.hl7.segments.util.ResourceUtils; + +// This class uses the ability to create ADDITIONAL HL7 messages to convert weird HL7 messages +// that exercise the new ignoreEmpty Resource Template functionality +// +// In these tests, the additional message definitions for (entirely ficticious) ADT^A09 and ^A10 messages +// are placed in src/test/resources/additional_resources/hl7/message/ADT_A09.yml and ADT_A10.yml + +class Hl7IgnoreEmptyTest { + + // # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + // NOTE VALIDATION IS INTENTIONALLY NOT USED BECAUSE WE ARE CREATING RESOURCES THAT ARE NOT STANDARD + private static final ConverterOptions OPTIONS = new Builder().withPrettyPrint().build(); + // # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + private static final String CONF_PROP_HOME = "hl7converter.config.home"; + + @TempDir + static File folder; + + static String originalConfigHome; + + @BeforeAll + static void saveConfigHomeProperty() { + originalConfigHome = System.getProperty(CONF_PROP_HOME); + ConverterConfiguration.reset(); + ResourceReader.reset(); + folder.setWritable(true); + } + + @AfterEach + void reset() { + System.clearProperty(CONF_PROP_HOME); + ConverterConfiguration.reset(); + ResourceReader.reset(); + } + + @AfterAll + static void reloadPreviousConfigurations() { + if (originalConfigHome != null) + System.setProperty(CONF_PROP_HOME, originalConfigHome); + else + System.clearProperty(CONF_PROP_HOME); + folder.setWritable(true); + } + + // ADT_A09 has ignoreEmpty = true; ADT_A10 doesn't + @ParameterizedTest + @CsvSource({ "ADT^A09,ZAL|\r,0", "ADT^A10,ZAL|\r,1", // Custom ZAL segment + "ADT^A09,AL1|\r,0", "ADT^A10,AL1|\r,1", // Standard AL1 segment + "ADT^A09,ZAL|||||||||||||||\r,0", // ZAL segment with all fields being empty + "ADT^A09,ZAL|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"\r,1" // So-called EMPTY ZAL segment is not actually empty + }) + void testIgnoreEmptySegment(String messageType, String emptySegment, int aiCount) throws IOException { + + // Set up the config file + commonConfigFileSetup(); + + // An empty AL1 and ZAL Segment... + String hl7message = "MSH|^~\\&|TestSystem||TestTransformationAgent||20150502090000||" + messageType + "|controlID|P|2.6\r" + + "EVN|A01|20150502090000|\r" + + "PID|||1234^^^^MR||DOE^JANE^|||F||||||||||||||||||||||\r" + + "PV1||I||||||||SUR||||||||S|VisitNumber^^^ACME|A||||||||||||||||||||||||20150502090000|\r" + + emptySegment; + + List e = getBundleEntryFromHL7Message(hl7message); + + List patientResource = ResourceUtils.getResourceList(e, ResourceType.Patient); + assertThat(patientResource).hasSize(1); // from PID + + List encounterResource = ResourceUtils.getResourceList(e, ResourceType.Encounter); + assertThat(encounterResource).hasSize(1); // from EVN, PV1 + + List allergyIntoleranceResource = ResourceUtils.getResourceList(e, ResourceType.AllergyIntolerance); + assertThat(allergyIntoleranceResource).hasSize(aiCount); // empty AL1 and ZAL Segments + + // Confirm that there are no extra resources + assertThat(e).hasSize(2 + aiCount); + + // We might be done + if(aiCount == 0) + return; + + // Let's take a peek at the 'empty' ZAL Segment + assertThat(allergyIntoleranceResource).allSatisfy(rs -> { + + // Only some of the fields + AllergyIntolerance ai = (AllergyIntolerance) rs; + assert(ai.hasId()); + assert(ai.hasClinicalStatus()); + assert(ai.hasVerificationStatus()); + }); + } + + + private static void commonConfigFileSetup() throws IOException { + File configFile = new File(folder, "config.properties"); + Properties prop = new Properties(); + prop.put("base.path.resource", "src/main/resources"); + prop.put("supported.hl7.messages", "ADT_A09, ADT_A10, ADT_A11"); // We're using weird ADT messages + prop.put("default.zoneid", "+08:00"); + // Location of additional resources + prop.put("additional.resources.location", "src/test/resources/additional_resources"); + prop.store(new FileOutputStream(configFile), null); + System.setProperty(CONF_PROP_HOME, configFile.getParent()); + } + + // Need custom convert sequence with options that turn off FHIR validation. + private static List getBundleEntryFromHL7Message(String hl7message) { + HL7ToFHIRConverter ftv = new HL7ToFHIRConverter(); // Testing loading of config which happens once per instantiation + String json = ftv.convert(hl7message, OPTIONS); // Need custom options that turn off FHIR validation. + assertThat(json).isNotNull(); + FHIRContext context = new FHIRContext(); + IBaseResource bundleResource = context.getParser().parseResource(json); + assertThat(bundleResource).isNotNull(); + Bundle b = (Bundle) bundleResource; + return b.getEntry(); + } + +} diff --git a/src/test/java/io/github/linuxforhealth/hl7/resource/HL7FHIRResourceConditionTest.java b/src/test/java/io/github/linuxforhealth/hl7/resource/HL7FHIRResourceConditionTest.java new file mode 100644 index 00000000..1cb35ea0 --- /dev/null +++ b/src/test/java/io/github/linuxforhealth/hl7/resource/HL7FHIRResourceConditionTest.java @@ -0,0 +1,128 @@ +/* + * (c) Te Whatu Ora, Health New Zealand, 2023 + * + * SPDX-License-Identifier: Apache-2.0 + */ +package io.github.linuxforhealth.hl7.resource; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.github.linuxforhealth.hl7.message.HL7FHIRResourceCondition; + +class HL7FHIRResourceConditionTest { + + + // These tests check that we can correctly parse Resource.condition string to make it ready for evaluation + @Test + void testResourceConditionComponentEQUALS() { + + HL7FHIRResourceCondition cond = new HL7FHIRResourceCondition("AL1.2.1 EQUALS 12"); + + assertThat(cond.fieldSpec).hasToString("[AL1.2.1]"); + assertThat(cond.op).isEqualTo(HL7FHIRResourceCondition.Operator.EQUALS); + assertThat(cond.values).hasSize(1); + assertThat(cond.values.get(0)).isEqualTo("12"); + } + + @Test + void testResourceConditionComponentNOTEQUALS() { + + HL7FHIRResourceCondition cond = new HL7FHIRResourceCondition("ZAL.2.1 NOT_EQUALS 12"); + + assertThat(cond.fieldSpec).hasToString("[ZAL.2.1]"); + assertThat(cond.op).isEqualTo(HL7FHIRResourceCondition.Operator.NOT_EQUALS); + assertThat(cond.values).hasSize(1); + assertThat(cond.values.get(0)).isEqualTo("12"); + } + + @Test + void testResourceConditionComponentIN() { + + HL7FHIRResourceCondition cond = new HL7FHIRResourceCondition("ZAL.2.1 IN [L3, L4, H1, H2]"); + + assertThat(cond.fieldSpec).hasToString("[ZAL.2.1]"); + assertThat(cond.op).isEqualTo(HL7FHIRResourceCondition.Operator.IN); + assertThat(cond.values).hasSize(4); + assertThat(cond.values.get(0)).isEqualTo("L3"); + assertThat(cond.values.get(1)).isEqualTo("L4"); + assertThat(cond.values.get(2)).isEqualTo("H1"); + assertThat(cond.values.get(3)).isEqualTo("H2"); + } + + @Test + void testResourceConditionComponentNOTIN() { + + HL7FHIRResourceCondition cond = new HL7FHIRResourceCondition("ZAL.2.1 NOT_IN [L3, L4, H1, H2]"); + + assertThat(cond.fieldSpec).hasToString("[ZAL.2.1]"); + assertThat(cond.op).isEqualTo(HL7FHIRResourceCondition.Operator.NOT_IN); + assertThat(cond.values).hasSize(4); + assertThat(cond.values.get(0)).isEqualTo("L3"); + assertThat(cond.values.get(1)).isEqualTo("L4"); + assertThat(cond.values.get(2)).isEqualTo("H1"); + assertThat(cond.values.get(3)).isEqualTo("H2"); + } + + @Test + void testResourceConditionSubComponentEQUALS() { + + HL7FHIRResourceCondition cond = new HL7FHIRResourceCondition("ZAL.3.2.1 EQUALS 13"); + + assertThat(cond.fieldSpec).hasToString("[ZAL.3.2.1]"); + assertThat(cond.op).isEqualTo(HL7FHIRResourceCondition.Operator.EQUALS); + assertThat(cond.values).hasSize(1).element(0).hasToString("13"); + } + + @Test + void testResourceConditionFieldEQUALS() { + + HL7FHIRResourceCondition cond = new HL7FHIRResourceCondition("ZAL.2 EQUALS 13"); + + assertThat(cond.fieldSpec).hasToString("[ZAL.2]"); + assertThat(cond.op).isEqualTo(HL7FHIRResourceCondition.Operator.EQUALS); + assertThat(cond.values).hasSize(1).element(0).hasToString("13"); + } + + @Test + void testResourceConditionSegmentEQUALS() { + + HL7FHIRResourceCondition cond = new HL7FHIRResourceCondition("ZAL EQUALS 13"); + + assertThat(cond.fieldSpec).hasToString("[ZAL]"); + assertThat(cond.op).isEqualTo(HL7FHIRResourceCondition.Operator.EQUALS); + assertThat(cond.values).hasSize(1).element(0).hasToString("13"); + } + + @Test + void testResourceConditionSegmentISNULL() { + + HL7FHIRResourceCondition cond = new HL7FHIRResourceCondition("ZAL.2.1 NULL"); + + assertThat(cond.fieldSpec).hasToString("[ZAL.2.1]"); + assertThat(cond.op).isEqualTo(HL7FHIRResourceCondition.Operator.NULL); + assertThat(cond.values).hasSize(0); + } + + @Test + void testResourceConditionSegmentNOTNULL() { + + HL7FHIRResourceCondition cond = new HL7FHIRResourceCondition("ZAL.2.1 NOT_NULL"); + + assertThat(cond.fieldSpec).hasToString("[ZAL.2.1]"); + assertThat(cond.op).isEqualTo(HL7FHIRResourceCondition.Operator.NOT_NULL); + assertThat(cond.values).hasSize(0); + } + + @Test + void testResourceConditionSegmentFieldRepetition() { + + HL7FHIRResourceCondition cond = new HL7FHIRResourceCondition("ZAL.2(1).1 NOT_NULL"); + + assertThat(cond.fieldSpec).hasToString("[ZAL.2(1).1]"); + assertThat(cond.op).isEqualTo(HL7FHIRResourceCondition.Operator.NOT_NULL); + assertThat(cond.values).hasSize(0); + } + +} diff --git a/src/test/resources/additional_resources/hl7/message/ADT_A09.yml b/src/test/resources/additional_resources/hl7/message/ADT_A09.yml index 282a9361..88f6b489 100644 --- a/src/test/resources/additional_resources/hl7/message/ADT_A09.yml +++ b/src/test/resources/additional_resources/hl7/message/ADT_A09.yml @@ -39,3 +39,19 @@ resources: - EVN - MSH - DG1 + + - resourceName: AllergyIntolerance + segment: AL1 + resourcePath: resource/AllergyIntolerance + repeats: true + ignoreEmpty: true ## We want to test our new ignoreEmpty flag + additionalSegments: + - MSH + + - resourceName: AllergyIntolerance + segment: ZAL + resourcePath: resource/AllergyIntolerance + repeats: true + ignoreEmpty: true ## We want to test our new ignoreEmpty flag on 'Z' segments + additionalSegments: + - MSH diff --git a/src/test/resources/additional_resources/hl7/message/ADT_A10.yml b/src/test/resources/additional_resources/hl7/message/ADT_A10.yml new file mode 100644 index 00000000..7c9db1d3 --- /dev/null +++ b/src/test/resources/additional_resources/hl7/message/ADT_A10.yml @@ -0,0 +1,57 @@ +# +# (C) Te Whatu Ora, Health New Zealand, 2023 +# +# SPDX-License-Identifier: Apache-2.0 +# +# FHIR Resources to extract from (pretend) ADT_A10 message +# + +######################################################################## +# Used for testing only. Not used in production. +######################################################################## + +--- +resources: + - resourceName: MessageHeader + segment: MSH + resourcePath: resource/MessageHeader + repeats: false + isReferenced: false + additionalSegments: + - EVN + + - resourceName: Patient + segment: PID + resourcePath: resource/Patient + repeats: false + isReferenced: true + additionalSegments: + - PD1 + - MSH + + - resourceName: Encounter + segment: PV1 + resourcePath: resource/Encounter + repeats: false + isReferenced: true + additionalSegments: + - PV2 + - EVN + - MSH + - DG1 + + - resourceName: AllergyIntolerance + segment: AL1 + resourcePath: resource/AllergyIntolerance + repeats: true + ignoreEmpty: false ## We want to test our new ignoreEmpty flag + additionalSegments: + - MSH + + - resourceName: AllergyIntolerance + segment: ZAL + resourcePath: resource/AllergyIntolerance + repeats: true + ignoreEmpty: false ## We want to test our new ignoreEmpty flag on 'Z' segments + additionalSegments: + - MSH diff --git a/src/test/resources/additional_resources/hl7/message/ADT_A11.yml b/src/test/resources/additional_resources/hl7/message/ADT_A11.yml new file mode 100644 index 00000000..e1a1e95c --- /dev/null +++ b/src/test/resources/additional_resources/hl7/message/ADT_A11.yml @@ -0,0 +1,93 @@ +# +# (C) Te Whatu Ora, Health New Zealand, 2023 +# +# SPDX-License-Identifier: Apache-2.0 +# +# FHIR Resources to extract from (pretend) ADT_A11 message +# + +######################################################################## +# Used for testing only. Not used in production. +######################################################################## + +--- +resources: + - resourceName: MessageHeader + segment: MSH + resourcePath: resource/MessageHeader + repeats: false + isReferenced: false + additionalSegments: + - EVN + + - resourceName: Patient + segment: PID + resourcePath: resource/Patient + repeats: false + isReferenced: true + additionalSegments: + - PD1 + - MSH + + - resourceName: Encounter + segment: PV1 + resourcePath: resource/Encounter + repeats: false + isReferenced: true + additionalSegments: + - PV2 + - EVN + - MSH + - DG1 + + - resourceName: AllergyIntolerance + segment: AL1 + resourcePath: resource/AllergyIntolerance + repeats: true + additionalSegments: + - MSH + + # SEGMENT - based condition + - resourceName: AllergyIntolerance + segment: ZSG + resourcePath: resource/AllergyIntolerance ## The AllergyIntolerance will be badly filled in + condition: ZSG EQUALS L2 + repeats: true + additionalSegments: + - MSH + + # FIELD - based condition + - resourceName: AllergyIntolerance + segment: ZFD + resourcePath: resource/AllergyIntoleranceZFD ## The AllergyIntolerance will be badly filled in + condition: ZFD.2 EQUALS L2 + repeats: true + additionalSegments: + - MSH + + # COMPONENT based condition + - resourceName: AllergyIntolerance + segment: ZCP + resourcePath: resource/AllergyIntoleranceZCP ## The AllergyIntolerance will be badly filled in + condition: ZCP.2.1 IN [A3, A4, H1, H3] + repeats: true + additionalSegments: + - MSH + + # COMPONENT based condition (repetition) + - resourceName: AllergyIntolerance + segment: ZCR + resourcePath: resource/AllergyIntoleranceZCP ## The AllergyIntolerance will be badly filled in + condition: ZCR.2(1).1 IN [A3, A4, H1, H3] + repeats: true + additionalSegments: + - MSH + + # SUB COMPONENT based condition + - resourceName: AllergyIntolerance + segment: ZSC + resourcePath: resource/AllergyIntoleranceZSC ## The AllergyIntolerance will be badly filled in + condition: ZSC.2.1.2 IN [A3, A4, H1, H3] + repeats: true + additionalSegments: + - MSH \ No newline at end of file diff --git a/src/test/resources/hl7/resource/AllergyIntoleranceZCP.yml b/src/test/resources/hl7/resource/AllergyIntoleranceZCP.yml new file mode 100644 index 00000000..b06bbac3 --- /dev/null +++ b/src/test/resources/hl7/resource/AllergyIntoleranceZCP.yml @@ -0,0 +1,48 @@ +# +# (c) Te Whatu Ora, Health New Zealand, 2023 +# +# SPDX-License-Identifier: Apache-2.0 +# +## pretend AllergyIntolerance from ZCP custom segment +## - actually for testing use of custom segment COMPONENT in conditions. +--- +resourceType: AllergyIntolerance + +type_1: + condition: $zcp32 EQUALS H1 || $zcp32 EQUALS H2 + generateList: false + expressionType: HL7Spec + valueOf: $code + vars: + zcp32: ZCP.3.2 + constants: + code: allergy + +type_2: + condition: $zcp32 NOT_EQUALS H1 && $zcp32 NOT_EQUALS H2 + generateList: false + expressionType: HL7Spec + valueOf: $code + vars: + zcp32: ZCP.3.2 + constants: + code: intolerance + +clinicalStatus: + valueOf: datatype/CodeableConcept_var + generateList: true + expressionType: resource + constants: + system: "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical" + code: "activeXXX" + display: "Active" + +verificationStatus: + valueOf: datatype/CodeableConcept_var + generateList: true + expressionType: resource + constants: + system: "http://terminology.hl7.org/CodeSystem/allergyintolerance-verification" + code: "confirmed" + display: "Confirmed" + diff --git a/src/test/resources/hl7/resource/AllergyIntoleranceZFD.yml b/src/test/resources/hl7/resource/AllergyIntoleranceZFD.yml new file mode 100644 index 00000000..d78c641d --- /dev/null +++ b/src/test/resources/hl7/resource/AllergyIntoleranceZFD.yml @@ -0,0 +1,48 @@ +# +# (c) Te Whatu Ora, Health New Zealand, 2023 +# +# SPDX-License-Identifier: Apache-2.0 +# +## pretend AllergyIntolerance from ZFD custom segment +## - actually for testing use of custom segment fields in conditions. +--- +resourceType: AllergyIntolerance + +type_1: + condition: $zfd3 EQUALS H1 || $zfd3 EQUALS H2 + generateList: false + expressionType: HL7Spec + valueOf: $code + vars: + zfd3: ZFD.3 + constants: + code: allergy + +type_2: + condition: $zfd3 NOT_EQUALS H1 && $zfd3 NOT_EQUALS H2 + generateList: false + expressionType: HL7Spec + valueOf: $code + vars: + zfd3: ZFD.3 + constants: + code: intolerance + +clinicalStatus: + valueOf: datatype/CodeableConcept_var + generateList: true + expressionType: resource + constants: + system: "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical" + code: "activeXXX" + display: "Active" + +verificationStatus: + valueOf: datatype/CodeableConcept_var + generateList: true + expressionType: resource + constants: + system: "http://terminology.hl7.org/CodeSystem/allergyintolerance-verification" + code: "confirmed" + display: "Confirmed" + diff --git a/src/test/resources/hl7/resource/AllergyIntoleranceZSC.yml b/src/test/resources/hl7/resource/AllergyIntoleranceZSC.yml new file mode 100644 index 00000000..96822cfd --- /dev/null +++ b/src/test/resources/hl7/resource/AllergyIntoleranceZSC.yml @@ -0,0 +1,48 @@ +# +# (c) Te Whatu Ora, Health New Zealand, 2023 +# +# SPDX-License-Identifier: Apache-2.0 +# +## pretend AllergyIntolerance from ZCP custom segment +## - actually for testing use of custom segment COMPONENT in conditions. +--- +resourceType: AllergyIntolerance + +type_1: + condition: $zsc322 EQUALS H1 || $zsc322 EQUALS H2 + generateList: false + expressionType: HL7Spec + valueOf: $code + vars: + zsc322: ZSC.3.2.2 + constants: + code: allergy + +type_2: + condition: $zsc322 NOT_EQUALS H1 && $zsc322 NOT_EQUALS H2 + generateList: false + expressionType: HL7Spec + valueOf: $code + vars: + zsc322: ZSC.3.2.2 + constants: + code: intolerance + +clinicalStatus: + valueOf: datatype/CodeableConcept_var + generateList: true + expressionType: resource + constants: + system: "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical" + code: "activeXXX" + display: "Active" + +verificationStatus: + valueOf: datatype/CodeableConcept_var + generateList: true + expressionType: resource + constants: + system: "http://terminology.hl7.org/CodeSystem/allergyintolerance-verification" + code: "confirmed" + display: "Confirmed" +