Russell Bateman February 2020 last update:
I sometimes find FHIRPath very challenging. For a long time, I had a JUnit test class that allowed me to dummy up FHIRPaths to try out against FHIR resources. Then a colleague pointed out this helper on GitHub:
HL7 / fhirpath.js
You have to paste in a JSON or YAML (FHIR) resource into the big window (it doesn't do XML which I read more easily). Then you can try out different FHIRPath "queries" against it. For example, I took the patient that was already there, then I tried out this FHIRPath:
Patient.name.where( use = 'official' ).given.first() + ' ' + Patient.name.given[ 1 ] + ' ' + Patient.name.family[ 0 ]
That netted me the response:
- Peter James Chalmers
I read JSON, but FHIR documents are so big and I learned FHIR many years ago when (at least I thought) everyone was doing XML at the time. So, here's a translator:
Your FHIR® in XML or JSON
Mind that you don't go translating anything PHI on-line.
Of course, we know how to parse patient data out of a FHIR document:
private static final String PATIENT = "" + "<Patient xmlns=\"http://hl7.org/fhir\">\n" + " <id value=\"9.PI\" />\n" + " <meta>\n" + " <lastUpdated value=\"2016-04-22T00:00:00.000-06:00\" />\n" + " </meta>\n" + " <name>\n" + " <family value=\"Munster\" />\n" + " <given value=\"Herman\" />\n" + " </name>\n" + " <gender value=\"male\" />\n" + " <birthDate value=\"1835-10-31\" />\n" + " <address>\n" + " <text value=\"1313 Mockingbird Lane, Mockingbird Heights, Beverly Hills, CA 90210\" />\n" + " <line value=\"1313 Mockingbird Lane\" />\n" + " <city value=\"Beverly Hills\" />\n" + " <state value=\"CA\"/>\n" + " <postalCode value=\"90210\" />\n" + " </address>\n" + "</Patient>";
@Test public void testStraight() { long start = System.currentTimeMillis(); final FhirContext context = FhirContext.forR4(); final IParser parser = context.newXmlParser(); final Patient patient = parser.parseResource( Patient.class, new ByteArrayInputStream( PATIENT.getBytes() ) ); for( HumanName name : patient.getName() ) System.out.println( name.getFamily() ); for( HumanName name : patient.getName() ) System.out.println( name.getFamily() + ", " + name.getGivenAsSingleString() ); System.out.println( patient.getBirthDate().toString() ); System.out.println( patient.getAddress().get( 0 ).getPostalCode() ); System.out.println( patient.getId() ); long end = System.currentTimeMillis(); System.out.println( "Time to run: " + ( end - start ) + " milliseconds." ); } /* output: Munster Munster, Herman Sat Oct 31 00:00:00 MST 1835 90210 Time to run: 38 milliseconds. */
However!
@Test public void testUsingFHIRPath() { long start = System.currentTimeMillis(); final FhirContext context = FhirContext.forR4(); final IParser parser = context.newXmlParser(); final Patient patient = parser.parseResource( Patient.class, new ByteArrayInputStream( PATIENT.getBytes() ) ); final String familyPath = "Patient.name.family"; List< StringType > family = context.newFhirPath().evaluate( patient, familyPath, StringType.class ); for( StringType f : family ) System.out.println( f ); final String namePath = "Patient.name"; List< HumanName > name = context.newFhirPath().evaluate( patient, namePath, HumanName.class ); for( HumanName n : name ) System.out.println( n.getFamily() + ", " + n.getGivenAsSingleString() ); final String dobPath = "Patient.birthDate.value"; List< StringType > dob = context.newFhirPath().evaluate( patient, dobPath, StringType.class ); for( StringType d : dob ) System.out.println( d ); final String zipPath = "Patient.address.postalCode"; List< StringType > zip = context.newFhirPath().evaluate( patient, zipPath, StringType.class ); for( StringType z : zip ) System.out.println( z ); final String idPath = "Patient.id.value"; Optional< StringType > id = context.newFhirPath().evaluateFirst( patient, idPath, StringType.class ); //noinspection OptionalGetWithoutIsPresent System.out.println( id.get() ); long end = System.currentTimeMillis(); System.out.println( "Time to run: " + ( end - start ) + " milliseconds." ); } /* output: Munster Munster, Herman 1835-10-31 90210 Time to run: 545 milliseconds. */
Notes on evaluate( IBase, String, Class ):
Here are the imports for the two tests above:
import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.List; import java.util.Optional; import org.junit.Test; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.parser.IParser; import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r4.model.Base; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.HumanName; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.utils.FHIRPathEngine; import com.windofkeltia.utilities.TestUtilities;
Here's an exhaustive sample in Java to find a FHIR Patient resource in a FHIR document (whose contents have already been read into variable CONTENTS) using FHIRPath. This includes priming the pump for the HAPI FHIR parser, reading in an parsing the FHIR document (usually a Bundle), priming the pump for the FHIRPath component and extracting a Patient resource from the FHIR document using a FHIRPath.
// priming the HAPI FHIR parser: FhirContext context = FhirContext.forR5(); IParser parser = context.newXmlParser(); // reading the FHIR document into a resource: BaseResource bundle = parser.parseResource( CONTENT.getBytes() ); // priming the pump for the FHIRPath component: DefaultProfileValidationSupport validation = new DefaultProfileValidationSupport( context ); IWorkerContext worker = new HapiWorkerContext( context, validation ); FHIRPathEngine fhirEngine = new FHIRPathEngine( worker ); // extracting a Patient resource from the FHIR document using FHIRPath: List< Base > patients = fhirEngine.evaluate( bundle, "Bundle.entry.resource.ofType(Patient)" ); * Patient patient = ( Patient ) patients.get( 0 );
For quick copy and paste:
FhirContext context = FhirContext.forR5(); IParser parser = context.newXmlParser(); BaseResource bundle = parser.parseResource( CONTENT.getBytes() ); DefaultProfileValidationSupport validation = new DefaultProfileValidationSupport( context ); IWorkerContext worker = new HapiWorkerContext( context, validation ); FHIRPathEngine fhirEngine = new FHIRPathEngine( worker ); List< Base > patients = fhirEngine.evaluate( bundle, "Bundle.entry.resource.ofType(Patient)" ); * Patient patient = ( Patient ) patients.get( 0 );
Once you've got a Patient, such as produced above, you can isolate a particular Identifier (for the reason of this, see Yet More FHIRPath...).
... // extracting a specific Identifier resource from the Patient using FHIRPath: List< Base > identifiers = fhirEngine.evaluate( patient, "Patient.identifier.where(extension('http://acme.us/fhir/extensions/mpi'))" ); Identifier identifier = identifiers.get( 0 );
Previously, you learned that this code will get you a patient out of a bundle:
FhirContext context = FhirContext.forR5(); IParser parser = context.newXmlParser(); BaseResource bundle = parser.parseResource( CONTENT.getBytes() ); DefaultProfileValidationSupport validation = new DefaultProfileValidationSupport( context ); IWorkerContext worker = new HapiWorkerContext( context, validation ); FHIRPathEngine engine = new FHIRPathEngine( worker ); List< Base > patients = fhirEngine.evaluate( bundle, "Bundle.entry.resource.ofType(Patient)" ); * Patient patient = ( Patient ) patients.get( 0 );
However, you must understand the cost to execute. The highlighted statements here cost, in my present development environment, 2 seconds or a bit more, while the elapsed time to execute the other statements is near 0.
Thus, you'll want to isolate your initialzation of the FHIRPath engine to a run-once or initialization segment of your application:
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r5.utils.FHIRPathEngine; . . . final FhirContext context = FhirContext.forR5(); final DefaultProfileValidationSupport validation = new DefaultProfileValidationSupport( context ); final IWorkerContext worker = new HapiWorkerContext( context, validation ); final FHIRPathEngine engine = new FHIRPathEngine( worker );
Experimentation: I do it the quick way, which is tantamount to writing HAPI FHIR code as if I know what I'm doing (well, okay, I do) then I resort to using FHIRPath three times, three different ways to achieve the same result. FHIRPath is going to be useful in the case where I "don't know what I'm doing," for example, the path is passed into me to look up in a resource (so I can't code sciently ahead).
Disclaimer: Since conquering the "cost" of using FHIRPath, I no longer do it the "quick way" since FHIRPath, once an engine is initialized, does not slow anything down.
The lines implementing a "direct," i.e.: sans FHIRPath, approach are highlighted here. The remaining lines employ the IFhirPath.evaluate() solution.
Imports to add to what's already being used are listed here.
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.hl7.fhir.r4.model.Medication; import org.hl7.fhir.r4.model.MedicationDispense; import org.hl7.fhir.r4.model.Resource; @Test public void testMedicationEmbeddedInMedicationDispense() throws IOException { final String PATHNAME = TestUtilities.TEST_RESOURCES + "flows/test-files/medicationdispense-3.xml"; final String CONTENT = TestUtilities.getLinesInFile( PATHNAME ); final FhirContext context = FhirContext.forR4(); final IParser parser = context.newXmlParser(); final MedicationDispense medicationDispense = parser.parseResource( MedicationDispense.class, new ByteArrayInputStream( CONTENT.getBytes() ) ); List< Resource > resources = medicationDispense.getContained(); assertFalse( resources.isEmpty() ); Resource resource = resources.get( 0 ); assertTrue( resource instanceof Medication ); Medication medication = ( Medication ) resource; System.out.println( medication.fhirType() ); final String medicationPath = "MedicationDispense.contained"; Optional< Medication > m = context.newFhirPath().evaluateFirst( medicationDispense, medicationPath, Medication.class ); System.out.println( m.get().fhirType() ); Optional< Resource > r = context.newFhirPath().evaluateFirst( medicationDispense, medicationPath, Resource.class ); System.out.println( r.get().fhirType() ); final String medicationPath2 = "MedicationDispense.contained.ofType(Medication)"; Optional< Medication > m2 = context.newFhirPath().evaluateFirst( medicationDispense, medicationPath2, Medication.class ); System.out.println( m2.get().fhirType() ); /* output Medication Medication Medication Medication */ }
FHIRPath is pretty powerful. You can do all sorts of crazy things. Here's a way to pluck a value out of an identifier that's concocted (proprietarily) to hold something called a "master patient index" or MPID. (An example of this in XML is given at the end of this note.)
package com.windofkeltia.hapi.fhir; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.List; import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.parser.IParser; import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r5.model.Base; import org.hl7.fhir.r5.model.BaseResource; import org.hl7.fhir.r5.model.Encounter; import org.hl7.fhir.r5.model.Extension; import org.hl7.fhir.r5.model.Identifier; import org.hl7.fhir.r5.model.Patient; import org.hl7.fhir.r5.utils.FHIRPathEngine; import com.windofkeltia.utilities.TestUtilities; public class ExploitFhirPathTest { @Rule public TestName name = new TestName(); @After public void tearDown() { } @Before public void setUp() { TestUtilities.setUp( name ); context = FhirContext.forR5(); parser = context.newXmlParser().setPrettyPrint( true ); fhirEngine = new FHIRPathEngine( new HapiWorkerContext( context, new DefaultProfileValidationSupport( context ) ) ); } private static final boolean VERBOSE = TestUtilities.verboseIfAttentive(); private FhirContext context; private IParser parser; private FHIRPathEngine fhirEngine; @Test public void test() throws IOException { final int LIMIT = 100000; final String FHIRPATH = "Patient.identifier.where(extension('http://acme.us/fhir/extensions/mpi')).value"; long usingFhirPath, goingDirect; Patient patient = getLilyMunster(); // measure FHIRPath elapsed time ------------------------------------------------------------------------------ usingFhirPath = System.currentTimeMillis(); for( int count = 0; count < LIMIT; count++ ) { List< Base > values = fhirEngine.evaluate( patient, FHIRPATH ); if( values.size() != 1 ) fail( "No MPID found" ); String mpid = String.valueOf( values.get( 0 ) ); } usingFhirPath = System.currentTimeMillis() - usingFhirPath; // measure direct-code elapsed time --------------------------------------------------------------------------- goingDirect = System.currentTimeMillis(); for( int count = 0; count < LIMIT; count++ ) { List< Identifier > identifiers = patient.getIdentifier(); Identifier mpidIdentifier = null; for( Identifier identifier : identifiers ) { List< Extension > extensions = identifier.getExtension(); for( Extension extension : extensions ) { String url = extension.getUrl(); if( url.equals( "http://acme.us/fhir/extensions/mpi" ) ) { mpidIdentifier = identifier; break; } } if( nonNull( mpidIdentifier ) ) break; } if( isNull( mpidIdentifier ) ) fail( "Unable to find desired extension URL" ); String mpid = mpidIdentifier.getValue(); if( mpid.isEmpty() ) fail( "No MPID found" ); } goingDirect = System.currentTimeMillis() - goingDirect; if( VERBOSE ) { System.out.println( "Benchmark, executing " + LIMIT + " iterations," ); System.out.println( " FHIRPath took " + usingFhirPath + " milliseconds" ); System.out.print ( " direct approach took " + goingDirect + " millisecond" ); System.out.println( ( goingDirect == 1 ) ? "" : "s" ); } } /** "Lily Munster" because that's the Patient's name in our test fodder. */ private Patient getLilyMunster() throws IOException { final String PATHNAME = TestUtilities.TEST_FODDER + "/lily-munster.xml"; final String CONTENT = TestUtilities.getLinesInFile( PATHNAME ); BaseResource resource = ( BaseResource ) parser.parseResource( new ByteArrayInputStream( CONTENT.getBytes() ) ); List< Base > patients = fhirEngine.evaluate( resource, "Bundle.entry.resource.ofType(Patient)" ); if( patients.isEmpty() ) fail( "Failed to find Patient Lily Munster" ); return ( Patient ) patients.get( 0 ); } }
I got the following output at one point:
Test: testBenchmark --------------------------------------------------------------------------------------- Benchmark, executing 100000 iterations, FHIRPath took 537 milliseconds direct approach took 7 milliseconds
As you can see, using FHIRPath, once you've set it up, is elegant, but there's certainly a substantial penalty.
Here's lily-munster.xml and the identifier that interests us in particular:
<Bundle xmlns="http://hl7.org/fhir"> <entry> <resource> <Patient xmlns="http://hl7.org/fhir"> <meta> <lastUpdated value="2021-01-05T00:00:00.000-06:00" /> </meta> <id value="9812850.PI"/> <identifier> <extension url="http://acme.us/fhir/extensions/mpi"> <extension url="PIRecord"> <valueString value="9812850.PI"/> </extension> </extension> <system value="https://test.acme.io/mpi/0/" /> <value value="665892"/> <assigner> <display value="Test Performance MPI system"/> </assigner> </identifier> <identifier> <system value="https://fhir.acme.io/facility/Beverly Hills Clinic" /> <value value="3660665800" /> <assigner> <display value="Beverly Hills Clinic" /> </assigner> </identifier> <name> <family value="Munster" /> <given value="Lily" /> </name> <gender value="female" /> <birthDate value="1827-04-01" /> <deceasedBoolean value="false" /> <address> <text value="1313 Mockingbird Lane, Mockingbird Heights, Beverly Hills, CA 90210" /> <line value="1313 Mockingbird Lane" /> <city value="Beverly Hills" /> <state value="CA"/> <postalCode value="90210" /> </address> <telecom> <system value="phone" /> <value value="+3035551212" /> <use value="home" /> </telecom> </Patient> </resource> </entry> <entry> <fullUrl value="urn:uuid:f78d73fc-9f9b-46d5-93aa-f5db86ba914c" /> <resource> <Encounter xmlns="http://hl7.org/fhir"> <class> <system value="http://terminology.hl7.org/CodeSystem/v3-ActCode" /> <code value="EMER" /> </class> <type> <coding> <system value="http://snomed.info/sct" /> <code value="410429000" /> <display value="Cardiac Arrest" /> </coding> <text value="Cardiac Arrest" /> </type> <subject> <reference value="urn:uuid:5cbc121b-cd71-4428-b8b7-31e53eba8184" /> <display value="Mrs. Lily Munster" /> </subject> <participant> <individual> <reference value="urn:uuid:0000016d-3a85-4cca-0000-000000000122" /> <display value="Victor von Frankenstein, M.D." /> </individual> </participant> <period> <start value="1965-11-15T06:22:41-05:00" /> <end value="1965-11-15T08:07:41-05:00" /> </period> <serviceProvider> <reference value="urn:uuid:8ad64ecf-c817-3753-bee7-006a8e662e06" /> <display value="Beverly Hills Hospital" /> </serviceProvider> </Encounter> </resource> <request> <method value="POST" /> <url value="Encounter" /> </request> </entry> </Bundle>
In an HL7v4 Observation and many other FHIR resources, there is a field, status whose type is often Observation.ObservationStatus, Claim.ClaimStatus, etc.
In most cases, when using a FHIRPath to reach a datum, you want to have to express the least amount of knowledge of the object (Observation, Account, etc.) possible. Yet, how to arrive at a datum documented thus? (channeling HL7FHIR Observation: Resource Content here)
status 1..1 code registered | preliminary | final | amended +
You're tempted to give into this:
FhirContext context = FhirContext.forR4(); IParser parser = context.newXmlParser().setPrettyPrint( true ); Claim claim = parser.parseResource( Claim.class, "<Claim ..." ); if( !claim.hasStatus() ) fail( "Test fodder needs a Claim." ); Object object = claim.getStatus(); if( object instanceof Claim.ClaimStatus ) { String status = ( ( Claim.ClaimStatus ) object ).getDisplay(); System.out.println( "Status: " + status ); }
The generic solution for handling this case is as follows. Note that the only appearance of "Observation" is in the FHIRPath, not in the Java code:
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.parser.IParser; import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r4.model.Base; import org.hl7.fhir.r4.model.Enumeration; import org.hl7.fhir.r4.utils.FHIRPathEngine; @Test public void test() { final String FHIRPATH = "Bundle.entry.resource.ofType(Observation).status"; FhirContext context = FhirContext.forR4(); IParser parser = context.newXmlParser().setPrettyPrint( true ); DefaultProfileValidationSupport validation = new DefaultProfileValidationSupport( context ); HapiWorkerContext worker = new HapiWorkerContext( context, validation ); FHIRPathEngine engine = new FHIRPathEngine( worker ); Bundle bundle = parser.parseResource( Bundle.class, BUNDLE ); List< Base > values = engine.evaluate( observation, FHIRPATH ); if( values.size() != 1 ) fail( "Unable to find " + FHIRPATH ); Object object = values.get( 0 ); System.out.println( "Object: " + object ); if( object instanceof Enumeration< ? > ) System.out.println( "Object is Enumeration< ? >" ); System.out.println( "String: " + String.valueOf( object ) ); Enumeration< ? > enumeration = ( Enumeration< ? > ) object; String e = enumeration.getValueAsString(); System.out.println( "Enumeration string: " + e ); }
Output from test():
Object: Enumeration[final] Object is Enumeration< ? > String: Enumeration[final] Enumeration string: final
The solution in the case of status, which is a code, is to exploit it as a generic Enumeration (of unknown type). I have left in verbose stabbing around in the code, which you can observe in the debugger, to show how to arrive at this conclusion because it's a principle that maps to other, similar type-difficult situations arising at the end of a FHIRPath.
Still, about 50% of datatypes or more at the end of any FHIRPath* can be resolved to strings thus:
final String FHIRPATH = "Bundle.entry.resource.ofType(Observation).valueInteger"; ... List< Base > values = engine.evaluate( observation, FHIRPATH ); if( values.size() != 1 ) fail( "Unable to find " + FHIRPATH ); Object object = values.get( 0 ); try { String value = values.get( 0 ); System.out.println( "Significant value: " + value ); return value; } catch( NullPointerException e ) { if( nonNull( logger ) ) logger.debug( "Extractor unable to harvest string from data type pointed at by the end of a FHIRPath" ); return null; } }
* Of course, we're talking about really generic FHIRPaths and not the very exact ones. Across the breadth of this topic, with my advice, your mileage may seriously vary.
Extract the references from the first subject of an Observation:
Bundle.entry.resource.ofType(Observation).subject[0].reference.value [Output] Patient/ebb16c62-cb06-4ff8-8ce8-ccb865e7a240
Extract the references from the first subject alternative:
Bundle.entry.resource.ofType(Observation).subject.first().reference.value [Output] Patient/ebb16c62-cb06-4ff8-8ce8-ccb865e7a240
Select the first Patient reference where the diastolic is over 90:
Bundle.entry.resource.ofType(Observation).where(component.where(code.coding.code = '8462-4').value.where( value > 90.0).exists()).subject.reference.value.first() [Output] Patient/5fabfccd-254e-42af-bed2-84199a5c05f2
Select the Patient's first name where the reference matches the logical id:
Bundle.entry.resource.ofType(Patient).where(id = '" + v.split("/")[1] + "').name.given.value [Output] John
Imagine that you're parsing a Bundle with a Patient in it and you want to extract data from different places that cannot be defined by a simple FHIRPath. The highlighted lines are the ones where we'll either verify a match or extracting a datum:
<Patient xmlns="http://hl7.org/fhir"> <id value="29632.PI"/> <identifier> <extension url="http://acme.us/fhir/extensions/mpid"> <extension url="PIRecord"> <valueString value="29632.PI"/> </extension> </extension> <system value="https://test.acme.io/mpi/0/"/> <value value="21240"/> <assigner> <display value="Acme Corporation Test"/> </assigner> </identifier> <identifier> <system value="https://github.com/synthetichealth/synthea"/> <value value="40b8f37e-385d-857f-2db0-07c142a8e4a2"/> </identifier> <name> <family value="Nikolaus26"/> <given value="Jesse626"/> <given value="Vincenzo126"/> </name> <birthDate value="1934-12-30"/> </Patient>
Here are two entries in an imaginary YAML configuration file that lead us to extracting keys/fields named pi (patient index) and mpid (master patient identity):
pi: Patient.identifier.where( $this.extension.url="http://acme.us/fhir/extensions/mpi" ).where( $this.extension.extension.url="PIRecord" ).extension.extension.value mpid : Patient.identifier.where( $this.extension.url="http://acme.us/fhir/extensions/mpi" ).value
The first FHIRPath will extract 29632.PI and the second 21240.
Because extensions are in the realm of "user-defined" FHIR, it's obvious that the Acme Corporation is saying that it's keeping these two entities in a FHIR Identifier marked specifically with a nested extension that disambiguates the Identifier from other Identifiers that might exist. The FHIRPaths enforce this by their .where() clauses.
Let's read the first one (rule that produces pi)...
For mpid, the extension-based condition of #2 is shared, but, back under the identifier is a value whose extracted datum provides mpid.
Life is hard. It's harder if you're stupid.
I don't mean to be demeaning or abrasive. These suggestions come at my own expense. When you're trying to analyze a FHIRPath against a FHIR resource and nothing you do works?
Patient.indentifier.where( system="https://github.com/synthetichealth/synthea" )
against a FHIR resource containing a Bundle?
<Bundle xmlns="http://hl7.org/fhir"> <entry> <resource>> <Patient xmlns="http://hl7.org/fhir"> <id value="2877.PI" /> <identifier> <system value="https://github.com/synthetichealth/synthea" /> <value value="29e51479-f742-4474-8f8e-d2607d5269f6" /> </identifier> <name> <use value="official" /> <family value="Connelly992" /> <given value="Abel832" /> <prefix value="Mr." /> </name> </Patient> </resource> </entry> </Bundle>
Patient.birthDate.value # as compared to Patient.name.text # (no value) Observation.category.coding.display # no .value and... Observation.category.text # no .value ...as compared to Observation.category.coding.code.value # which needs .value
List< Base > values; Object value; try { values = fhirEngine.evaluate( resource, condition ); if( !values.isEmpty() ) value = values.get( 0 ); } catch( FHIRException e ) { logger.warn( e.getMessage() ); }
To avoid confusion, let's note these examples. These apply to all instances of polymorphism ([x]) in FHIR.
... maritalStatus 0..1 CodeableConcept multipleBirth[x] 0..1 multipleBirthBoolean boolean multipleBirthInteger integer photo 0..* Attachment ...
<Patient xmlns="http://hl7.org/fhir"> ... <multipleBirthBoolean value="true" /> <!-- or, instead: --> <multipleBirthInteger value="3" /> ... </Patient>
Patient.multipleBirth
However, when composing FHIR in XML (also JSON), follow these rules:
In the (IntelliJ IDEA) debugger, you will see:
import org.hl7.fhir.r4.model.Base; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.IntegerType; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.utils.FHIRPathEngine; private FHIRPathEngine fhirEngine; void extract( Resource resource, String fhirPath ) { List< Base > values = fhirEngine.evaluate( resource, fhirPath ); if( values.isEmpty() ) fail( "No datum was reached using FHIRPath \" + fhirPath + "\"" ); String value = null; Object object = values.get( 0 ); // which of boolean or integer was it? if( object instanceof BooleanType ) value = ( ( BooleanType ) object ).asStringValue(); else if( object instance of IntegerType ) value = ( ( IntegerType ) object ).getValueAsString(); ...
For reference, here are the primitive FHIR datatypes. An instance of any of these should return true in the following execution:
Base object = ( one of these ); if( object.isPrimitive() ) ...
To see the value held by such an object, the following will convert it to a string:
String value = ( ( PrimitiveType< ? > ) value).asStringValue();
There are two problems with FHIRPath. Let's illustrate by pretending we're implementing the creation of "fieldnames" plus FHIRPaths into a resource to give those fieldnames a value. For our example, let's assume FHIR Observation and, within that, we want effectiveDateTime plus interpretation.
effectiveDate : Observation.effective
interpretation : Observation.interpretation
import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.model.Base; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.utils.FHIRPathEngine; public class Foo { public void fooBar( Resource resource, final String fhirPath ) { List< Base > values; try { values = fhirEngine.evaluate( resource, fhirPath ); } catch( FHIRException e ) { StringBuilder message = new StringBuilder(); message.append( e.getMessage() ).append( ":\n " ).append( fhirPath ).append( "\nSkipping..." ); logger.debug( message.toString() ); return; } if( values.isEmpty() ) // (FHIRPath resolved to no datum) continue; Base object = values.get( 0 ); String value = object.toString(); if( !object.isPrimitive() ) // this is pretty clever... { StringBuilder message = new StringBuilder(); message.append( "Fieldname: " ).append( fieldname ) .append( ", FHIRPath: " ).append( fhirPath ); message.append( System.lineSeparator() ); message.append( " Object is not a primitive type and cannot be the terminal element of a FHIRPath. " ); message.append( "It will be necessary to resort to a more complex FHIRPath. Skipping..." ); continue; } try // ...and this is even more clever! { value = ( ( PrimitiveType< ? > ) object ).asStringValue(); } catch( Exception e ) { logger.warn( "Failed to convert {} to simple string for {}, because ", object, fhirPath, e ); continue; } } }
Note that Observation's effective[x] includes
Of the 4 different possibilities for what's to be put there, two of them are pretty simple to translate (1 and 4) using this FHIRPath:
Observation.effective
The other two require much more work, better done in FHIRPath than in Java. Here is an example for getting the starting date of the observation if what's recorded is a Period:
Observation.effective.ofType( Period ).start
This works because start is one of those easily handled scalars (DateTimeType), so the FHIRPath can stop there.
"Functions" are syntactically expressed facilities that appear in the FHIRPath and aid in reaching a particular datum wanted.
Here is a summary to tease the imagination, but you can find the definitive treatment here: FHIRPath.
Filter the input collection for items named "extension" with the given url. This is a syntax shortcut for .extension.where(url = string), but is simpler to write. Will return an empty collection if the input collection is empty or the url is empty.
Returns true if the input collection contains a single value which is a FHIR primitive.
Return the underlying system value for the FHIR primitive if the input collection contains a single value which is a FHIR primitive, and it has a primitive value (see discussion for hasValue()). Otherwise the return value is empty.
For each item in the collection, if it is a string that is a uri (or canonical or url), locate the target of the reference, and add it to the resulting collection.
An alias for ofType() maintained purely for backwards compatibility.
Returns a collection that contains all items in the input collection that are of the given type or a subclass thereof.
Returns the FHIR element definition information for each element in the input collection.
Returns the given slice as defined in the given structure definition. The structure argument is a uri.
For each element in the input collection, verifies that there are no modifying extensions defined other than the ones given by the modifier argument (comma-separated string).
Returns true if the single input element conforms to the profile specified by the structure argument, and false otherwise.
When invoked on a single code-valued element, returns true if the code is a member of the given valueset.
When invoked on a Coding-valued element and the given code is Coding-valued, returns true if the source code is equivalent to the given code, or if the source code subsumes the given code.
When invoked on a Coding-valued element and the given code is Coding-valued, returns true if the source code is equivalent to the given code.
When invoked on a single xhtml element returns true if the rules around HTML usage are met, and false if they are not.
This function returns the lowest possible value in the natural range expressed by the type it is invoked on.
This function returns true if the engine executing the FHIRPath statement can compare the singleton Quantity with the singleton other Quantity and determine their relationship to each other.