Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature: Conditional Resource Templates #507

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 88 additions & 2 deletions TECHNIQUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -260,4 +284,66 @@ fundingSource:
- valueOf: datatype/CodeableConcept
expressionType: resource
specs: OBX.5
```
```

## 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:

```
<hl7spec> EQUALS | NOT_EQUALS | IN | NOT_IN | NULL | NOT_NULL <value> | [ ... ]
```
**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`
19 changes: 19 additions & 0 deletions TEMPLATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
```

Expand All @@ -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. |

Expand Down Expand Up @@ -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:

```

Expand Down Expand Up @@ -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<br>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
25 changes: 25 additions & 0 deletions src/main/java/io/github/linuxforhealth/api/ResourceCondition.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public SimpleEvaluationResult(V value, List<ResourceValue> 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<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,21 @@ public SimpleBiCondition(String var1, String var2, String conditionOperator) {
@Override
public boolean test(Map<String, EvaluationResult> 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");
Expand All @@ -49,12 +59,16 @@ public boolean test(Map<String, EvaluationResult> 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")
Expand Down Expand Up @@ -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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -64,7 +70,13 @@ public boolean isReferenced() {
return isReferenced;
}

@Override
public boolean ignoreEmpty() {
return ignoreEmpty;
}



@Override
public String conditionExpression() {
return condition;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,21 @@ 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();
}
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()) {
Expand Down
Loading