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.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>
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 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:
- 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: $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.
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.
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
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 thespecs: DG1
.
- Inner Structure (
expressionsMap
)- Cycles through each
specs: $Condition
resource created from each DG1 segment processed in the parent messageADT_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 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.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