Skip to content

Latest commit

 

History

History
263 lines (225 loc) · 10.7 KB

TECHNIQUES.md

File metadata and controls

263 lines (225 loc) · 10.7 KB

LinuxForHealth HL7 to FHIR Converter Hints and Techniques

Overview

Additional information, techniques, and hints about development of templates, java exits, and other enhancements to the converter.

Getting Java control via data Types

The converter extracts information via a template and converts to a data type, such as STRING and BOOLEAN. You can take advantage of this and create custom data types which can be called for processing of unique inputs. Before the input is used, your custom data type processor will be invoked.

As an example, type RELIGIOUS_AFFILIATION_CC was added to SimpleDataTypeMapper.java and mapped to the data handler method RELIGIOUS_AFFILIATION_FHIR_CC in SimpleDataValueResolver.java

The template reference passes input PID.17 through custom data type RELIGIOUS_AFFILIATION_CC:

extension_2:
  condition: $valCodeableConcept NOT_NULL
  valueOf: extension/Extension_CodeableConcept
  generateList: true
  expressionType: resource
  vars:
    url: String, GeneralUtils.getExtensionUrl("religion")
    valCodeableConcept: RELIGIOUS_AFFILIATION_CC, PID.17

The mapping.

java
RELIGIOUS_AFFILIATION_CC(SimpleDataValueResolver.RELIGIOUS_AFFILIATION_FHIR_CC),

The resolver takes as input a value and returns a CodeableConcept object, which can be used in the template yaml. In the example code, the input value is converted to a string and the HAPI FHIR V3ReligiousAffiliation.class is used to lookup the code. With the code, the V3ReligiousAffiliation is found from the code and is used to create the CodeableConcept.

    public static final ValueExtractor<Object, CodeableConcept> RELIGIOUS_AFFILIATION_FHIR_CC = (Object value) -> {
        String val = Hl7DataHandlerUtil.getStringValue(value);
        String code = getFHIRCode(val, V3ReligiousAffiliation.class);
        if (code != null) {
            V3ReligiousAffiliation rel = V3ReligiousAffiliation.fromCode(code);
            CodeableConcept codeableConcept = new CodeableConcept();
            codeableConcept.addCoding(new Coding(rel.getSystem(), code, rel.getDisplay()));
            codeableConcept.setText(rel.getDisplay());
            return codeableConcept;
        } else {
            return null;
        }
    };

The lookup of the FHIR code is in file v2ToFFhirMapping.yml and it is important to note that the lookup section is the same as the class passed in getFHIRCode(val, V3ReligiousAffiliation.class)

V3ReligiousAffiliation:
  # Agnostic -> Agnosticism
  AGN: 1004
  # Atheist -> Athiesm
  ATH: 1007
  # Baha'i -> Babi & Baha'I faiths
  BAH: 1008
  <etc>

Getting Java control via JEXL calls to classes

Evaluation of JEXL expressions can include the evaluation of a custom method. You can use this to get control and evaluate an input before returning from the JEXL evaluation.

As an example, address district has specialized rules for when Parish should be used. The Address template evaluates a JEXL expression that calls to the public getAddressDistrict which is in file Hl7RelatedGeneralUtils.java, which is mapped to GeneralUtils. Variables are collected and input to method.

Address.yml

district:
     type: STRING
     valueOf: 'GeneralUtils.getAddressDistrict( patientCounty, addressCountyParish, patient)'
     expressionType: JEXL
     vars:
          patientCounty: String, PID.12
          addressCountyParish: String, XAD.9
          patient: PID

Hl7RelatedGeneralUtils.getAddressDistrict

public static String getAddressDistrict(String patientCountyPid12, String addressCountyParishPid119, Object patient) {
        LOGGER.info("getAddressCountyParish for {}", patient);

        String returnDistrict = addressCountyParishPid119;
        if (returnDistrict == null) {
            Segment pidSegment = (Segment) patient;
            try {
                Type[] addresses = pidSegment.getField(11);
                if (addresses.length == 1) {
                    returnDistrict = patientCountyPid12;
                }
            } catch (HL7Exception e) {
                // Do nothing.  Just eat the error.
                // Let the initial value stand
            }
        }
        return returnDistrict;
    }

Time

Time conversion and formatting utilities are available, but they must be called in a thread-safe way. The time ZoneId may be specified as a default in config.properties value default.zoneid=+08:00 or passed in via runtime context using .withZoneIdText("-05:00"). The value of the input ZoneIdText is available as the system variable $ZONEID and should be used in all time conversions where a time zone is needed. GeneralUtils.dateTimeWithZoneId takes an input HL7 time value field and converts it using the ZoneIdText of the current context. The timezone should not be stored by any static method, accessing the ZoneIdText via the system variable makes the time processing threadsafe.

time:
  type: STRING
  valueOf: "GeneralUtils.dateTimeWithZoneId(dateTimeIn,ZONEID)"
  expressionType: JEXL
  vars:
    dateTimeIn: NTE.6 | NTE.7

The rules for determining a time zone for a date time value are:

  1. If a DateTime from HL7 contains it's own ZoneId in the DateTime, use it.
  2. If it has no ZoneId, and the context ZoneIdText is set, use that.
  3. If it has no ZoneId, and no context ZoneIdText, but there is a config ZoneId, use that.
  4. If it has no ZoneId, and no context ZoneIdText, and no config ZoneId, use the local timezone (whichis the ZoneId of the server where the process is running).

YAML Hints

Hints about the ways syntax and references work in the YAML files

Condition test variables, not templates

Testing the segment fields directly in conditions doesn't work. Instead you must create a var for the template field and test the var.

Not this:

telecom_1:
    condition: PID.14 NOT_NULL    
    valueOf: datatype/ContactPoint
    generateList: true
    expressionType: resource
    specs: PID.14
    constants: 
       use: "work"

Do this:

telecom_1:
    condition: $pid14 NOT_NULL    
    valueOf: datatype/ContactPoint
    generateList: true
    expressionType: resource
    specs: PID.14
    vars:
       pid14: PID.14
    constants: 
       use: "work"

Referencing resources

Resources are referenced (linked) in one of two ways:

To reference an resource already created:

subject:
   valueOf: datatype/Reference
   expressionType: resource
   specs: $Patient

Note the expressionType is resource of template datatype/Reference. specs is the resource from the message yaml: $Patient. Not the $ capital letter preceding the variable. The created $Patient is passed to the Reference template as content.

To create a new resource and reference a resource:

 performer:
   valueOf: resource/Practitioner
   expressionType: reference
   specs: OBX.16

Note the expression is a reference. Practitioner is the template which will be used with the specs OBX.16 to create a Practitioner and create a reference to it in this position.

Passing references to children

Consider PPR_PC1.yml. Note the resourceNames: ServiceRequest and Encounter.

In DocumentReference.yml, references to ServiceRequest and Encounter resources already created are passed as serviceRequestRef and encounterRef.

context:
   valueOf: secondary/Context
   expressionType: resource
   vars:
      timestamp: TXA.4 | OBR.7
      providerCode: TXA.5
      serviceRequestRef: $ServiceRequest
      encounterRef: $Encounter

Then in Context.yml, the reference are used as specs passed into to resource expressions to datatype/Reference. Thus the already created ServiceRequest and Encounter are used.

related_2:
  valueOf: datatype/Reference
  expressionType: resource
  generateList: true
  specs: $serviceRequestRef

encounter:
  condition: $encounterRef NOT_NULL
  valueOf: datatype/Reference
  expressionType: resource
  specs: $encounterRef

Referencing resource values that are created from repeating segments

In cases where we need to match specific resources that are in a list from a spec, we can use a nested structure where the outer part identifies the element to match, and the inner part uses condition(s) to find the matching element. A good example of this is Encounter.diagnosis in Encounter.yml.

  • Outer Structure
    • Nested expression type.
    • Cycles through each DG1 segment via specs: DG1.
    • Creates an identifier in $refDG13 from DG1.3 for each time we cycle through the specs: DG1.
  • Inner Structure (expressionsMap)
    • Cycles through each specs: $Condition resource created from each DG1 segment processed in the parent message ADT_A03.
    • Matches the $refDG13 Identifier from the outer structure to the $refconditionId condition identifier from the inner structure.
    • $refconditionId is found in the $Condition by using GeneralUtils.extractAttribute, which uses a pattern matching utility to find the identifier.
diagnosis:
   expressionType: nested
   evaluateLater: true
   generateList: true
   specs: DG1
   vars:
      refDG13: BUILD_IDENTIFIER_FROM_CWE, DG1.3
   expressionsMap:
     condition:
        valueOf: datatype/Reference
        expressionType: resource
        condition: $refconditionId EQUALS_STRING $refDG13 # Inner loop (refconditionId) matches outer loop (refDG13)
        specs: $Condition # Loops over the entire list of Condition resources
        vars:
          # refconditionId is calculated by pattern matching to find identifier that contains urn:id:extID as the system"
          refconditionId: $BASE_VALUE, GeneralUtils.extractAttribute(refconditionId,"$.identifier[?(@.system==\"urn:id:extID\")].value","String")

Referencing fields of repeating segments

As another example, in Immunization, each of the OBX segments needed processing of OBX.5 fields based on the value of OBX.3. The following looks like it would work, but it doesn't. On the surface, it appears that specs: OBX.5 will take each OBX record and process OBX5. However this only works for the first OBX.5, because specs: OBX.5 is really specifying to repeat the sub-fields of OBX.5.

# Wrong way to repeat for all OBX records
fundingSource:
  valueOf: datatype/CodeableConcept
  expressionType: resource
  condition: $obx3b EQUALS 30963-3
  specs: OBX.5
  vars:
    obx3b: String, OBX.3.1

The solution is to repeat on OBX using spec: OBX, and nest the OBX.5 processing within the repeat from spec: OBX

# Right way to repeat for all OBX records
fundingSource:
   expressionType: nested
   condition: $obx3b EQUALS 30963-3
   specs: OBX
   vars:
      obx3b: String, OBX.3.1 
   expressions:
      - valueOf: datatype/CodeableConcept
        expressionType: resource
        specs: OBX.5