Additional information, techniques, and hints about development of templates, java exits, and other enhancements to the converter.
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.17The 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>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: PIDHl7RelatedGeneralUtils.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 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.7The rules for determining a time zone for a date time value are:
- If a DateTime from HL7 contains it's own ZoneId in the DateTime, use it.
- If it has no ZoneId, and the context ZoneIdText is set, use that.
- If it has no ZoneId, and no context ZoneIdText, but there is a config ZoneId, use that.
- 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).
Hints about the ways syntax and references work in the YAML files
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"Resources are referenced (linked) in one of two ways:
subject:
valueOf: datatype/Reference
expressionType: resource
specs: $PatientNote 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.
performer:
valueOf: resource/Practitioner
expressionType: reference
specs: OBX.16Note 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.
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: $EncounterThen 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: $encounterRefIn 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
$refDG13from DG1.3 for each time we cycle through thespecs: DG1.
- Inner Structure (
expressionsMap)- Cycles through each
specs: $Conditionresource created from each DG1 segment processed in the parent messageADT_A03. - Matches the
$refDG13Identifier from the outer structure to the$refconditionIdcondition identifier from the inner structure. $refconditionIdis found in the$Conditionby usingGeneralUtils.extractAttribute, which uses a pattern matching utility to find the identifier.
- Cycles through each
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")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.1The 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