Russell Bateman February 2020 last update:
Finally getting around to starting a page of notes on using HAPI-FHIR.
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; import org.hl7.fhir.r5.model.Bundle; public class HapiFhirRoundTrip { private static final String PATIENT_AND_ENCOUNTER = + "<Bundle xmlns=\"http://hl7.org/fhir\">\n" + " <type value=\"transaction\" />\n" + " <entry>\n" + " <fullUrl value=\"urn:uuid:38826a3f-8e5c-4846-95c3-7f12a233447d\" />\n" + " <resource>\n" + " <Patient xmlns=\"http://hl7.org/fhir\">\n" + " <meta>\n" + " <lastUpdated value=\"2021-01-05T00:00:00.000-06:00\" />\n" + " </meta>\n" + " <identifier>\n" + " <system value=\"https://fhir.acme.io/facility/Beverly Hills Clinic\" />\n" + " <value value=\"3660665800\" />\n" + " <assigner>\n" + " <display value=\"Beverly Hills Clinic\" />\n" + " </assigner>\n" + " </identifier>\n" + " <name>\n" + " <family value=\"Munster\" />\n" + " <given value=\"Herman\" />\n" + " </name>\n" + " <gender value=\"male\" />\n" + " <birthDate value=\"1835-10-31\" />\n" + " <deceasedBoolean value=\"false\" />\n" + " <address>\n" + " <text value=\"1313 Mockingbird Lane, Mockingbird Heights, CA 90210\" />\n" + " <line value=\"1313 Mockingbird Lane\" />\n" + " <city value=\"Mockingbird Heights\" />\n" + " <state value=\"CA\" />\n" + " <postalCode value=\"90210\" />\n" + " </address>\n" + " <telecom>\n" + " <system value=\"phone\" />\n" + " <value value=\"+3035551212\" />\n" + " <use value=\"home\" />\n" + " </telecom>\n" + " </Patient>\n" + " </resource>\n" + " </entry>\n" + " <entry>\n" + " <resource>\n" + " <Encounter xmlns=\"http://hl7.org/fhir\">\n" + " <id value=\"emerg\" />\n" + " <period>\n" + " <start value=\"2017-02-01T08:45:00+10:00\" />\n" + " <end value=\"2017-02-01T09:27:00+10:00\" />\n" + " </period>\n" + " </Encounter>\n" + " </resource>\n" + " <request>\n" + " <method value=\"POST\" />\n" + " <url value=\"Patient\" />\n" + " </request>\n" + " </entry>\n" + "</Bundle>\n"; @Test public void test() { FhirContext context = FhirContext.forR5(); IParser parser = context.newXmlParser().setPrettyPrint( true ); Bundle bundle = ( Bundle ) parser.parseResource( PATIENT_AND_ENCOUNTER ); System.out.println( parser.encodeResourceToString(bundle ) ); } }
<Bundle xmlns="http://hl7.org/fhir"> <type value="transaction"/> <entry> <fullUrl value="urn:uuid:38826a3f-8e5c-4846-95c3-7f12a233447d"/> <resource> <Patient xmlns="http://hl7.org/fhir"> <meta> <lastUpdated value="2021-01-05T00:00:00.000-06:00"/> </meta> <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="Herman"/> </name> <telecom> <system value="phone"/> <value value="+3035551212"/> <use value="home"/> </telecom> <gender value="male"/> <birthDate value="1835-10-31"/> <deceasedBoolean value="false"/> <address> <text value="1313 Mockingbird Lane, Mockingbird Heights, CA 90210"/> <line value="1313 Mockingbird Lane"/> <city value="Mockingbird Heights"/> <state value="CA"/> <postalCode value="90210"/> </address> </Patient> </resource> </entry> <entry> <resource> <Encounter xmlns="http://hl7.org/fhir"> <id value="emerg"/> </Encounter> </resource> <request> <method value="POST"/> <url value="Patient"/> </request> </entry> </Bundle>
This is a "how-to" here...
This is nasty. If you google "hapi fhir javadoc" or even "hapi fhir humanname javadoc" you end up with a list of links the best of which lands you only here:
https://hapifhir.io/hapi-fhir/docs/appendix/javadocs.html
From there, it's frustrating. What you need to understand is tricks to get you over the next couple of hyperlink traversals and into the meat. Probably, because what you're looking for is data structures that help you anaylzed what is parsed, think "model":
https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-structures-r4/
Then (keep thinking "model"):
https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-structures-r4/org/hl7/fhir/r4/model/package-summary.html
and, finally, you can start looking seriously, e.g.: (in Chrome) Ctrl-F, type "humanname":
https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-structures-r4/org/hl7/fhir/r4/model/HumanName.html
Then type "period" (if you're looking to figure out HAPI FHIR's Period type (start and end dates) as consumed for a name, an address or a telecom in the FHIR Patient record.
I find this infuriatingly "thick" given how easy other frameworks make it (although, in fairness, for years to look at NiFi Javadoc, you pretty well had to pull down the source code--we only use the binary downloads--and build it yourself).
When you run the HAPI FHIR parser, if you don't know the type of resource that's in the document and you don't care, just use IBaseResource. If we did care, we might substitute Bundle or Patient or MedicationRequest, etc. in place of IBaseResource.
Note that conveniently there are three versions of IParser.parseResource() for FHIR as a String, FHIR on an input stream and FHIR in a reader:
...and, almost as conveniently, two versions of IParser.encodeResourceXyz():
String document; // the FHIR content switch( EncodingEnum.detectEncoding( document ) ) { case XML : break; case JSON : break; case RDF : break; }
Assume that we know the document is in XML.
String document; // the FHIR content IParser parser = context.newXmlParser(); IBaseResource resource = parser.parseResource( document );
By junk, I mean extra elements under a resource that are not FHIR, that is, they are not documented at Resource Index or below as linked.
Now, the FHIR validator will log them as bad. However, HAPI FHIR is a serious tool for getting work done. If it blew chunks for every tiny mistake, it's easy to see that it would be very hard to write a parsed analysis of an incoming document.
Someone in the HAPI FHIR group mentioned that he had problems with ugly XML generation in HAPI FHIR whenever the STaX library was missing. I need to check into that. It turns out that adding this to pom.xml is the solution:
<dependency> <groupId>org.codehaus.woodstox</groupId> <artifactId>woodstox-core-asl</artifactId> <version>4.2.0</version> </dependency>
Here we are brute-force generating an identifier such as might go underneath a Patient:
public static void injectIdentifierThingIntoPatient( Patient patient ) { // create <type><coding>... CodeableConcept codeableConcept = new CodeableConcept(); Coding coding = new Coding(); coding.setSystem( "https://fhir.acme.io/fhir/code/thing" ); coding.setCode( "thing" ); coding.setDisplay( "Thing stuff..." ); // and the array to keep them in... List< Coding > codings = new ArrayList<>(); codings.add( coding ); codeableConcept.setCoding( codings ); // create the <assigner>... Reference assigner = new Reference(); assigner.setDisplay( "Patient thing" ); // create the top-level <identifier> and link the guts in... Identifier thing = new Identifier(); thing.setUse( Identifier.IdentifierUse.USUAL ); thing.setType( codeableConcept ); thing.setSystem( "https://debug.acme.io/thing/0" ); thing.setValue( "99" ); thing.setAssigner( assigner ); // now link it into the patient our caller gave us... List< Identifier > identifiers = patient.getIdentifier(); identifiers.add( thing ); patient.setIdentifier( identifiers ); }
Generated, it looks something like this:
<identifier> <use value="usual" /> <type> <coding> <system value="http://fhir.acme.us/fhir/codes/thing" /> <code value="thing" /> <display value="Thing stuff..." /> </coding> </type> <system value="https://debug.acme.io/mpi/0/" /> <value value="99" /> <assigner> <display value="Patient thing"/> </assigner> </identifier>
If this happens frequently enough, it could be usefully rolled into a static utility class using a Builder Pattern. The Builder interface would be something like:
import java.util.ArrayList; import java.util.List; import static java.util.Objects.nonNull; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; public class FhirIdentifier { private final String codeableCode; private final String codeableSystem; private final String codeableDisplay; private final String assignerDisplay; private final String system; private final String value; private final Identifier.IdentifierUse use; protected FhirIdentifier( Builder builder ) { codeableCode = builder.codeableCode; codeableSystem = builder.codeableSystem; codeableDisplay = builder.codeableDisplay; assignerDisplay = builder.assignerDisplay; system = builder.system; value = builder.value; use = builder.use; } public void injectInto( Patient patient ) // maybe soften this if Identifiers can go into other FHIR resources { // create <type><coding>... CodeableConcept codeableConcept = new CodeableConcept(); Coding coding = new Coding(); if( nonNull( codeableCode ) ) coding.setCode( codeableCode ); if( nonNull( codeableSystem ) ) coding.setSystem( codeableSystem ); if( nonNull( codeableDisplay ) ) coding.setDisplay( codeableDisplay ); // and the array to keep them in... List< Coding > codings = new ArrayList<>(); codings.add( coding ); codeableConcept.setCoding( codings ); // create the top-level <identifier> and link the guts in... Identifier thing = new Identifier(); if( nonNull( use ) ) thing.setUse( use ); thing.setType( codeableConcept ); if( nonNull( system ) ) thing.setSystem( system ); if( nonNull( value ) ) thing.setValue( value ); // create the <assigner>... if( nonNull( assignerDisplay ) ) { Reference assigner = new Reference(); assigner.setDisplay( assignerDisplay ); thing.setAssigner( assigner ); } // now link it into the patient our caller gave us... List< Identifier > identifiers = patient.getIdentifier(); identifiers.add( thing ); patient.setIdentifier( identifiers ); } private static Identifier.IdentifierUse getUse( final String use ) { return Identifier.IdentifierUse.valueOf( use ); } public static class Builder { private String codeableCode; private String codeableSystem; private String codeableDisplay; private String assignerDisplay; private String system; private String value; private Identifier.IdentifierUse use; public Builder codeableCode( String code ) { this.codeableCode = code; return this; } public Builder codeableSystem( String system ) { this.codeableSystem = system; return this; } public Builder codeableDisplay( String display ) { this.codeableDisplay = display; return this; } public Builder assignerDisplay( String display ) { this.assignerDisplay = display; return this; } public Builder system( String system ) { this.system = system; return this; } public Builder value( String value ) { this.value = value; return this; } public Builder use( String use ) { this.use = getUse( use ); return this; } public FhirIdentifier build() { return new FhirIdentifier( this ); } } }
...and used like this:
FhirIdentifier identifier = new FhirIdentifier.Builder() .codeableCode( "thing" ) .codeableSystem( "http://fhir.acme.us/fhir/codes/thing" ) .codeableDisplay( "Thing stuff..." ) .system( "https://debug.acme.io/mpi/0/" ) .value( "99" ) .assignerDisplay( "Patient thing" ) .use( "usual" ) .build(); identifier.injectInto( patient );
Parsing this:
...we do this:
import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Patient; . . . public class FunWithPatientIdentifiersAndExtensions { public void analyzeIdentifierWithExtensions( Patient patient ) { for( Identifier identifier : patient.getIdentifier() ) { Extension extension = identifier.getExtensionByUrl( "http://fhir.acme.us/fhir/extensions/mpi" ); if( isNull( extension ) ) break; MPID = identifier.getValue(); Extension piExtension = extension.getExtensionByUrl( "PIRecord" ); PI = String.valueOf( piExtension.getValue() ); // (reason for this example) break; } }
Creating what's in bold here:
<identifier> <type> <coding> <extension url="http://acme.us/fhir/codes/"> <extension url="systemdisplay"> <valueString value="SNOMED" /> </extension> </extension> <system value="http://snomed.info/sct" /> <code value="38341003" /> <display value="Hypertension" /> </coding> <text value="Hypertension" /> </type> </identifier>
for( Coding coding : codings ) { if( coding.getSystem().equals( "http://snomed.info/sct" ) ) { // add an extesion to help human-readability... Extension subExtension = new Extension(); subExtension.setUrl( "systemdisplay" ); subExtension.setValue( new StringType( "SNOMED" ) ); Extension extension = new Extension(); extension.setUrl( "http://windofkeltia.com/fhir/codes/" ); extension.addExtension( subExtension ); coding.addExtension( extension ); } }
If you wish to update HAPI FHIR, e.g.: from 5.1.0 to 5.2.0, you will almost surely get this message when you run (JUnit tests, etc.):
java.lang.IllegalStateException: Could not find the HAPI-FHIR structure JAR on the classpath for version R5. Note that as of HAPI-FHIR v0.8, a separate FHIR strcture JAR must be added to your classpath (or project pom.xml if you are using Maven)
and you'll have to:
Was working on FHIRPath a couple of weeks ago. I have decided to put this work here. There is precious little documentation on how to use FHIRPath on the HAPI FHIR website or even Googling "x"fhirpath examples" and "fhirpathengine examples" which is a fruitless exercise.
This is a simple way to get started. James Agnew calls this "agnostic FHIRPath" as compared to using HapiWorkerContext (as I do later on).
FhirContext context = FhirContext.forR5(); IFhirPath path = context.newFhirPath();
package com.windofkeltia; import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.fhirpath.IFhirPath; import ca.uhn.fhir.parser.IParser; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r5.model.Base; import org.hl7.fhir.r5.model.Bundle; import org.hl7.fhir.r5.model.Patient; import org.hl7.fhir.r5.model.StringType; import org.hl7.fhir.r5.utils.FHIRPathEngine; import utilities.TestUtilities; /** * @author Russell Bateman * @since January 2021 */ public class FhirPathTest { @Rule public TestName name = new TestName(); @After public void tearDown() { } @Before public void setUp() { TestUtilities.setUp( name ); } private static final boolean VERBOSE = TestUtilities.VERBOSE; private static final String FHIR_CONTENT = "" + "<Bundle xmlns=\"http://hl7.org/fhir\">\n" + " <type value=\"transaction\"/>\n" + " <entry>\n" + " <fullUrl value=\"urn:uuid:38826a3f-8e5c-4846-95c3-7f12a233447d\"/>\n" + " <resource>\n" + " <Patient xmlns=\"http://hl7.org/fhir\">\n" + " <meta>\n" + " <lastUpdated value=\"2021-01-05T00:00:00.000-06:00\" />\n" + " </meta>\n" + " <identifier>\n" + " <system value=\"https://fhir.acme.io/facility/Beverly Hills Clinic\" />\n" + " <value value=\"3660665800\" />\n" + " <assigner>\n" + " <display value=\"Beverly Hills Clinic\" />\n" + " </assigner>\n" + " </identifier>\n" + " <name>\n" + " <family value=\"Munster\" />\n" + " <given value=\"Herman\" />\n" + " </name>\n" + " <gender value=\"male\" />\n" + " <birthDate value=\"1835-10-31\" />\n" + " <deceasedBoolean value=\"false\" />\n" + " <address>\n" + " <text value=\"1313 Mockingbird Lane, Mockingbird Heights, CA 90210\" />\n" + " <line value=\"1313 Mockingbird Lane\" />\n" + " <city value=\"Mockingbird Heights\" />\n" + " <state value=\"CA\"/>\n" + " <postalCode value=\"90210\" />\n" + " </address>\n" + " <telecom>\n" + " <system value=\"phone\" />\n" + " <value value=\"+3035551212\" />\n" + " <use value=\"home\" />\n" + " </telecom>\n" + " </Patient>\n" + " </resource>\n" + " </entry>\n" + " <entry>\n" + " <resource>\n" + " <Encounter xmlns=\"http://hl7.org/fhir\">\n" + " <id value=\"emerg\" />\n" + " <period>\n" + " <start value=\"2017-02-01T08:45:00+10:00\" />\n" + " <end value=\"2017-02-01T09:27:00+10:00\" />\n" + " </period>\n" + " </Encounter>\n" + " </resource>\n" + " <request>\n" + " <method value=\"POST\" />\n" + " <url value=\"Patient\" />\n" + " </request>\n" + " </entry>\n" + "</Bundle>\n"; @Test public void testBundle() { FhirContext context = FhirContext.forR5(); IParser parser = context.newXmlParser(); IFhirPath where = context.newFhirPath(); Bundle bundle = ( Bundle ) parser.parseResource( FHIR_CONTENT ); List< IBaseResource > resources = where.evaluate( bundle, "Bundle.entry.resource", IBaseResource.class ); System.out.println( resources ); } /* output: [org.hl7.fhir.r5.model.Patient@5ea502e0, org.hl7.fhir.r5.model.Encounter@443dbe42] */ @Test public void testPatient() { FhirContext context = FhirContext.forR5(); IParser parser = context.newXmlParser(); IFhirPath where = context.newFhirPath(); Bundle bundle = ( Bundle ) parser.parseResource( FHIR_CONTENT ); for( Bundle.BundleEntryComponent component : bundle.getEntry() ) { IBaseResource resource = component.getResource(); if( !( resource instanceof Patient ) ) continue; Patient patient = ( Patient ) resource; List< StringType > familyNames = where.evaluate( patient, "Patient.name.family", StringType.class ); System.out.println( familyNames.get( 0 ) ); } } /* output: Munster */ @Test public void testFun() { FhirContext context = FhirContext.forR5(); IParser parser = context.newXmlParser(); Bundle bundle = parser.parseResource( Bundle.class, FHIR_CONTENT ); FHIRPathEngine fhirPathEngine = new FHIRPathEngine( new HapiWorkerContext( context, new DefaultProfileValidationSupport( context ) ) ); List< Base > familyNames = fhirPathEngine.evaluate( bundle.getEntry().get( 0 ).getResource(), "Patient.name.family" ); List< Base > dateTypes = fhirPathEngine.evaluate( bundle.getEntry().get( 0 ).getResource(), "Patient.birthDate" ); System.out.println( familyNames.get( 0 ).toString() ); System.out.println( dateTypes.get( 0 ).dateTimeValue().getValueAsString() ); } /* output: Munster 1835-10-31 */ @Test public void testFamilyName() { FhirContext context = FhirContext.forR5(); IParser parser = context.newXmlParser(); Bundle bundle = parser.parseResource( Bundle.class, FHIR_CONTENT ); FHIRPathEngine fhirPathEngine = new FHIRPathEngine( new HapiWorkerContext( context, new DefaultProfileValidationSupport( context ) ) ); List< Base > patients = fhirPathEngine.evaluate( bundle.getEntry().get( 0 ).getResource(), "Patient" ); Patient patient = ( Patient ) patients.get( 0 ); List< Base > familyNames = fhirPathEngine.evaluate( patient, "Patient.name.family" ); System.out.println( familyNames.get( 0 ).toString() ); String familyName; familyName = fhirPathEngine.evaluateToString( bundle.getEntry().get( 0 ).getResource(), "Patient.name.family" ); System.out.println( familyName ); familyName = fhirPathEngine.evaluateToString( patient, "Patient.name.family" ); System.out.println( familyName ); int y = 9; } /* output: Munster Munster Munster */ @Test public void testBirthdate() { FhirContext context = FhirContext.forR5(); IParser parser = context.newXmlParser(); Bundle bundle = parser.parseResource( Bundle.class, FHIR_CONTENT ); FHIRPathEngine fhirPathEngine = new FHIRPathEngine( new HapiWorkerContext( context, new DefaultProfileValidationSupport( context ) ) ); List< Base > patients = fhirPathEngine.evaluate( bundle.getEntry().get( 0 ).getResource(), "Patient" ); Patient patient = ( Patient ) patients.get( 0 ); List< Base > birthDates = fhirPathEngine.evaluate( patient, "Patient.birthDate" ); System.out.println( birthDates.get( 0 ).dateTimeValue().getValueAsString() ); } /* output: 1835-10-31 */ }
The official FHIR validator is a useful JAR whose usage is explained at this hyperlink. The JAR is available at this link. All the details on use, and they are numerous and confusing, are available from that page. The validator JAR can be easily made accessible from the command line thus:
#!/bin/sh if [ "$1" = "--help" ]; then echo "Usage: $0 [-output ]" exit 0 fi VALIDATOR_PATH="/home/russ/dev/fhir-validator/validator_cli.jar" java -jar $VALIDATOR_PATH -version 4.0 $*
Use is this way:
~/bin $ ./fhir-validator.sh --help Usage: ./fhir-validator.sh [-output ] ~/bin $ fhir-validator.sh FHIR-document validator-output FHIR Validation tool Version 5.1.17 (Git# 44f7dca1c794). Built 2020-10-14T20:04:32.293Z (11 days old) Java: 1.8.0_265 from /usr/lib/jvm/java-8-openjdk-amd64/jre on amd64 (64bit). 3538MB available Paths: Current = /home/russ/bin, Package Cache = /home/russ/.fhir/packages Params: -version 4.0 mds-extract Loading Load FHIR v4.0 from hl7.fhir.r4.core#4.0.1 - 4575 resources (00:04.0220) Terminology server http://tx.fhir.org - Version 1.0.362 (00:01.0186) Get set... go (00:00.0003) Validating Validate mds-extract ..Detect format for mds-extract . . .
Here are the Maven dependencies for the code about to come. (This isn't the whole pom.xml.) This was the latest version of HAPI-FHIR at the time I wrote this. In particular, I'm guided by HAPI-FHIR Documentation, specifically, the link Parsing and Serializing. All this HAPI-FHIR software was written by or under the ægis of James Agnew—huge kudos to him.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <properties> <hapi-fhir.version>4.2.0</hapi-fhir.version> </properties> <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-base</artifactId> <version>${hapi-fhir.version}</version> </dependency> <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-structures-dstu3</artifactId> <version>${hapi-fhir.version}</version> </dependency> </project>
package com.windofkeltia.fhir; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; import org.hl7.fhir.dstu3.model.Patient; import com.windofkeltia.utilities.TestUtilities; /** * Prime this pump: figure out how HAPI FHIR works from our direction: usually, consumers are, * unlike us, interested in the client-side or in FHIR servers set up to validate what they * produce. We're not here to produce FHIR (XML or JSON), but to parse it and turn it into * a meta- or interlingua for a search engine (not shown here). * * @author Russell Bateman * @since February 2020 */ public class FhirContextTest { // @formatter:off @Rule public TestName name = new TestName(); @Before public void setUp() { TestUtilities.setUp( name ); } @After public void tearDown() { } private static boolean VERBOSE = true;//TestUtilities.VERBOSE; /** * The purpose of this case is only to demonstrate the client side of things a little bit * (see {@link FhirContextTest} documentation), which isn't what interests us, but doing * this does give us an easy way to come up with the XML for a Patient. We could also * generate the JSON for a sample Patient. Whatever we made in here, we could use in one * of the more germane tests below. */ @Ignore @Test public void testXmlSerialization() { FhirContext context = FhirContext.forDstu3(); IParser parser = context.newXmlParser().setPrettyPrint( true ); Patient patient = new Patient(); patient.addName().setFamily( "Simpson" ).addGiven( "James" ); String serialized = parser.encodeResourceToString( patient ); System.out.println( " Patient (serialized):\n" + serialized ); } @Test public void testXml() { FhirContext context = FhirContext.forDstu3(); final String INPUT = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" + "<Patient xmlns=\"http://hl7.org/fhir\">\n" + " <name>\n" + " <family value=\"Simpson\" />\n" + " <given value=\"James\" />\n" + " </name>\n" + "</Patient>"; if( VERBOSE ) System.out.println( " Input:\n" + INPUT ); IParser parser = context.newXmlParser(); Patient patient = parser.parseResource( Patient.class, INPUT ); if( VERBOSE ) { System.out.print( " " + patient.getName().get( 0 ).getFamily() ); System.out.println( ", " + patient.getName().get( 0 ).getGiven() ); } } @Test public void testJson() { FhirContext context = FhirContext.forDstu3(); final String INPUT = "{" + " \"resourceType\" : \"Patient\"," + " \"name\" :" + " [" + " {" + " \"family\": \"Simpson\"" + " }" + " ]" + "}"; if( VERBOSE ) System.out.println( " Input:\n " + INPUT ); IParser parser = context.newJsonParser(); Patient patient = parser.parseResource( Patient.class, INPUT ); if( VERBOSE ) { System.out.print( " " + patient.getName().get( 0 ).getFamily() ); System.out.println( ", " + patient.getName().get( 0 ).getGiven() ); } } /** * Start out in XML, but switch to JSON for output (serialization). */ @Test public void testXmlToJson() { FhirContext context = FhirContext.forDstu3(); final String INPUT = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" + "<Patient xmlns=\"http://hl7.org/fhir\">" + " <name>" + " <family value=\"Simpson\" />" + " <given value=\"James\" />" + " </name>" + "</Patient>"; if( VERBOSE ) System.out.println( " Input:\n" + INPUT ); IParser parser = context.newXmlParser(); Patient patient = parser.parseResource( Patient.class, INPUT ); parser = context.newJsonParser().setPrettyPrint( true ); String serialized = parser.encodeResourceToString( patient ); if( VERBOSE ) System.out.println( " Pretty-printed JSON output:\n" + serialized ); } }
Sample output from these tests
Test: testXml ---------------------------------------------------------------------------------- [main] INFO ca.uhn.fhir.util.VersionUtil - HAPI FHIR version 4.2.0 - Rev 8491707942 [main] INFO ca.uhn.fhir.context.FhirContext - Creating new FHIR context for FHIR version [DSTU3] Input: <?xml version="1.0" encoding="UTF-8" standalone="no"?> <Patient xmlns="http://hl7.org/fhir"> <name> <family value="Simpson"/> <given value="James"/> </name> </Patient> Simpson, [James] Test: testJson --------------------------------------------------------------------------------- [main] INFO ca.uhn.fhir.context.FhirContext - Creating new FHIR context for FHIR version [DSTU3] Input: { "resourceType" : "Patient", "name" : [ { "family": "Simpson" } ]} Simpson, [] Test: testXmlToJson ---------------------------------------------------------------------------- [main] INFO ca.uhn.fhir.context.FhirContext - Creating new FHIR context for FHIR version [DSTU3] Input: <?xml version="1.0" encoding="UTF-8" standalone="no"?><Patient xmlns="http://hl7.org/fhir"><name><family value="Simpson"/><given value="James"/></name></Patient> Pretty-printed JSON output: { "resourceType": "Patient", "name": [ { "family": "Simpson", "given": [ "James" ] } ] }
package com.windofkeltia.fhir; import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.fhirpath.IFhirPath; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.IValidatorModule; import ca.uhn.fhir.validation.SingleValidationMessage; import ca.uhn.fhir.validation.ValidationResult; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r5.model.Bundle; import com.windofkeltia.utilities.TestUtilities; /** * @author Russell Bateman * @since April 2021 */ public class FhirValidationTest { @Rule public TestName name = new TestName(); @After public void tearDown() { } @Before public void setUp() { TestUtilities.setUp( name ); } private static final boolean VERBOSE = TestUtilities.VERBOSE; private static final String PATIENT_AND_ENCOUNTER = "" // DO NOT CHANGE THIS! + "<Bundle xmlns=\"http://hl7.org/fhir\">\n" + " <type value=\"transaction\"/>\n" + " <entry>\n" + " <fullUrl value=\"urn:uuid:38826a3f-8e5c-4846-95c3-7f12a233447d\"/>\n" + " <resource>\n" + " <Patient xmlns=\"http://hl7.org/fhir\">\n" + " <meta>\n" + " <lastUpdated value=\"2021-01-05T00:00:00.000-06:00\" />\n" + " </meta>\n" + " <identifier>\n" + " <system value=\"https://fhir.acme.io/facility/Beverly Hills Clinic\" />\n" + " <value value=\"3660665800\" />\n" + " <assigner>\n" + " <display value=\"Beverly Hills Clinic\" />\n" + " </assigner>\n" + " </identifier>\n" + " <name>\n" + " <family value=\"Munster\" />\n" + " <given value=\"Herman\" />\n" + " </name>\n" + " <gender value=\"male\" />\n" + " <birthDate value=\"1835-10-31\" />\n" + " <deceasedBoolean value=\"false\" />\n" + " <address>\n" + " <text value=\"1313 Mockingbird Lane, Mockingbird Heights, CA 90210\" />\n" + " <line value=\"1313 Mockingbird Lane\" />\n" + " <city value=\"Mockingbird Heights\" />\n" + " <state value=\"CA\"/>\n" + " <postalCode value=\"90210\" />\n" + " </address>\n" + " <telecom>\n" + " <system value=\"phone\" />\n" + " <value value=\"+3035551212\" />\n" + " <use value=\"home\" />\n" + " </telecom>\n" + " </Patient>\n" + " </resource>\n" + " </entry>\n" + " <entry>\n" + " <resource>\n" + " <Encounter xmlns=\"http://hl7.org/fhir\">\n" + " <id value=\"emerg\" />\n" + " <period>\n" + " <start value=\"2017-02-01T08:45:00+10:00\" />\n" + " <end value=\"2017-02-01T09:27:00+10:00\" />\n" + " </period>\n" + " </Encounter>\n" + " </resource>\n" + " <request>\n" + " <method value=\"POST\" />\n" + " <url value=\"Patient\" />\n" + " </request>\n" + " </entry>\n" + "</Bundle>\n"; @Test public void testBundle() { FhirContext context = FhirContext.forR5(); IParser parser = context.newXmlParser().setPrettyPrint( true ); IFhirPath where = context.newFhirPath(); Bundle bundle = ( Bundle ) parser.parseResource( PATIENT_AND_ENCOUNTER ); List< IBaseResource > resources = where.evaluate( bundle, "Bundle.entry.resource", IBaseResource.class ); if( VERBOSE ) System.out.println( parser.encodeResourceToString( bundle ) ); } @Test public void testBundleOnValidator() { FhirContext context = FhirContext.forR5(); IParser parser = context.newXmlParser().setPrettyPrint( true ); Bundle bundle = ( Bundle ) parser.parseResource( PATIENT_AND_ENCOUNTER ); IValidatorModule module = new FhirInstanceValidator( context ); FhirValidator validator = context.newValidator().registerValidatorModule( module ); ValidationResult result = validator.validateWithResult( bundle ); if( VERBOSE ) { System.out.println( PATIENT_AND_ENCOUNTER ); final String PROMPT = "<Bundle ... />"; for( SingleValidationMessage message : result.getMessages() ) System.out.println( message.getLocationString() + " " + message.getMessage() ); } } }
Output from the validator (formatted as a table):
The main downfall is not to have line numbers. HAPI FHIR has been planning line numbers for time immemorial, but admits that it's a lot of work and won't happen soon.
This is a new test case for FhirValidationTest above.
@Test public void testBundleOnValidationSupportChain() { FhirContext context = FhirContext.forR5(); IParser parser = context.newXmlParser().setPrettyPrint( true ); Bundle bundle = ( Bundle ) parser.parseResource( PATIENT_AND_ENCOUNTER ); IValidatorModule module = new FhirInstanceValidator( context ); FhirValidator validator = context.newValidator().registerValidatorModule( module ); ValidationSupportChain supportChain = new ValidationSupportChain( new DefaultProfileValidationSupport( context ), new InMemoryTerminologyServerValidationSupport( context ), new CommonCodeSystemsTerminologyService( context ) ); FhirInstanceValidator instanceValidator = new FhirInstanceValidator( supportChain ); instanceValidator.setAnyExtensionsAllowed( true ); ValidationResult result = validator.validateWithResult( bundle ); if( VERBOSE ) { int count = 1; System.out.println( " Validation was " + result.isSuccessful() ); System.out.println( PATIENT_AND_ENCOUNTER ); final String PROMPT = "<Bundle ... />"; for( SingleValidationMessage message : result.getMessages() ) System.out.println( StringUtilities.padStringRight( ""+count++, 4 ) + message.getSeverity() + " - " + message.getLocationString() + " " + message.getMessage() ); } }
1 ERROR - Bundle bdl-3: entry.request mandatory for batch/transaction/history, allowed for subscription-notification, otherwise prohibited [entry.all(request.exists() = ((%resource.type = 'batch') or (%resource.type = 'transaction') or (%resource.type = 'history'))) or (type = 'subscription-notification')] 2 ERROR - Bundle.entry[0].resource.ofType(Patient).identifier[0].system URI values cannot have whitespace('https://fhir.acme.io/facility/Beverly Hills Clinic') 3 ERROR - Bundle.entry[1].resource.ofType(Encounter) Profile http://hl7.org/fhir/StructureDefinition/Encounter, Element 'Bundle.entry[1].resource.ofType(Encounter).status': minimum required = 1, but only found 0 4 ERROR - Bundle.entry[1].resource.ofType(Encounter) Profile http://hl7.org/fhir/StructureDefinition/Encounter, Element 'Bundle.entry[1].resource.ofType(Encounter).class': minimum required = 1, but only found 0
I left some nicer bread crumbs on validation here.
This is a switch from extracting information from and validating existing FHIR. To follow this code, you must imagine a POJO, IxmlPatient, that contains information recorded in another framework from which a FHIR patient is to be created.
Some of this is simplified to keep the example short, but it's fairly complete as compared to HAPI FHIR documentation.
import java.util.ArrayList; import java.util.List; import org.hl7.fhir.r5.model.BooleanType; import org.hl7.fhir.r5.model.DataType; import org.hl7.fhir.r5.model.IntegerType; import org.hl7.fhir.r5.model.Patient; import com.windofkeltia.ixml.pojos.IxmlPatient; public class PopulatePatient { private final Patient patient; public Patient get() { return patient; } public PopulatePatient( IxmlPatient ixmlPojo ) { Identifier identifier = new Identifier(); identifier.setUse( Identifier.IdentifierUse.OFFICIAL ); identifier.setValue( mrn ); patient.addIdentifier( identifier.get() ); patient.setActive( true ); name.setUse( HumanName.NameUse.OFFICIAL ); name.setFamily( pojo.lastname ); List< String > givens = new ArrayList<>(); givens.add( pojo.firstname ); givens.addAll( pojo.middlename ); for( String given : gives ) name.addGiven( given ); name.addPrefix( pojo.prefix ); name.addSuffix( pojo.suffix ); patient.addName( name ); ContactPoint telecom = new ContactPoint(); telecom.setUse( ContactPoint.ContactPointUse.HOME ); telecom.setSystem( ContactPoint.ContactPointSystem.PHONE ); telecom.setValue( pojo.phone ); telecom.setRank( 1 ); patient.addTelecom( telecom ); patient.setGender( ( pojo.gender.equals( "F" ) ) ? Enumerations.AdministrativeGender.FEMALE : Enumerations.AdministrativeGender.MALE ); patient.setBirthDate( makeDateFromString( pojo.birthdate ) ); patient.setDeceased( new BooleanType( false ) ); Address address = new Address(); address.setUse( Address.AddressUse.HOME ); address.setType( Address.AddressType.POSTAL ); address.addLine( pojo.street ); address.setCity( pojo.city ); address.setState( pojo.state ); address.setPostalCode( pojo.zipcode ); address.setCountry( pojo.country ); patient.addAddress( address ); patient.setMaritalStatus( ( pojo.maritalstatus.equals( "married" ) ) ? "M" : "U" ); if( pojo.multipleBirth != 0 ) { patient.setMultipleBirth( new BooleanType( true ) ); patient.setMultipleBirth( new IntegerType( pojo.multipleBirth ) ); } Patient.ContactComponent contact = new Patient.ContactComponent(); CodeableConcept codeableConcept = new CodeableConcept(); Coding coding = new Coding(); coding.setCode( "N" ); coding.setSystem( "http://terminology.hl7.org/CodeSystem/v2-0131" ); coding.setDisplay( "next-of-kin" ); coding.setVersion( "2.9" ); codeableConcept.addCoding( coding ); contact.addRelationship( codeableConcept ); contact.setName( name ); contact.addTelecom( telecom ); contact.setAddress( address ); contact.setGender( ( pojo.nok.gender.equals( "F" ) ) ? Enumerations.AdministrativeGender.FEMALE : Enumerations.AdministrativeGender.MALE ); patient.addContact( contact ); Patient.PatientCommunicationComponent communication = new Patient.PatientCommunicationComponent(); CodeableConcept languageCodeablConcept = new CodeableConcept(); Coding languageCoding = new Coding(); languageCoding.setSystem( "http://hl7.org/fhir/ValueSet/all-languages" ); languageCoding.setVersion( "4.01" ); languageCoding.setDisplay( pojo.preferredlanguage ); communication.setLanguage( languageCodeableConcept ); communication.setPreferred( true ); // need to add reference to 'generalPractitioner' (Organization|Practitioner|PractitionerRole) // need to add reference to 'managingOrganization' (Organization) }
I cheat a little bit above because I inherit patient and other data in an intermediate form I call IXML whose origin is in old work with HL7 v3. I should give background to my examples by showing it though it's super simple. Here's what a minimal patient looks like. (No PHI here—all of it is made up.)
package com.windofkeltia.ixml; import java.util.Collections; import com.windofkeltia.ixml.pojos.IxmlAddress; import com.windofkeltia.ixml.pojos.IxmlLanguage; import com.windofkeltia.ixml.pojos.IxmlMaritalStatus; import com.windofkeltia.ixml.pojos.IxmlPatient; import com.windofkeltia.ixml.pojos.IxmlReligion; import com.windofkeltia.ixml.pojos.IxmlSex; public class IxmlDataMockUps { public static IxmlPatient makePatient() { IxmlPatient patient = new IxmlPatient(); patient.facilityoid = "2.16.840.1.113883.3.6452"; patient.mrn = "665892"; patient.birthdate = "1827-04-01"; patient.firstname = "Lily"; patient.lastname = "Munster"; patient.gender = new IxmlSex().withGender( "F" ); patient.language = new IxmlLanguage().withLanguage( "English" ); patient.maritalstatus = new IxmlMaritalStatus().withMaritalStatus( "MARRIED" ); patient.religion = new IxmlReligion().withReligion( "LTH" ); IxmlAddress address = new IxmlAddress( "address" ); address.line = Collections.singletonList( "1313 Mockingbird Lane" ); address.city = "Mockingbird Heights"; address.state = "CA"; address.zipcode = "90210"; address.country = "US"; patient.address = address; return patient; } ... }
We'll take the FHIR Patient we created earlier and embed it in a FHIR Bundle. Why is this interesting? Because there are fields in the bundle to set up and also an entry/resource envelop the patient must live in.
public class PopulateBundleTest { private static final boolean VERBOSE = true; @Test public void wrapWithResourceEntry() throws FhirServerException { Patient patient = new PopulatePatient( ixmlPojo ).generate(); Bundle bundle = new Bundle(); Identifier identifier = new Identifier(); identifier.setUse( Identifier.IdentifierUse.OFFICIAL ); identifier.setValue( patient.getIdentifierFirstRep().getValue() ); bundle.setIdentifier( identifier ); bundle.setType( Bundle.BundleType.SEARCHSET ); BundleEntryRequestComponentBuilder requestBuilder = new BundleEntryRequestComponentBuilder.Builder() .method( "get" ) .url( "this is the request URL" ) .ifMatch( "if match" ) .build(); BundleEntryComponentBuilder entryBuilder = new BundleEntryComponentBuilder.Builder() .fullUrl( "this is a URL" ) .resource( patient ) .search( "match" ) .request( requestBuilder.get() ) .build(); bundle.addEntry( entryBuilder.get() ); // encode into XML string and write this out... HapiFhirInstance instance = HapiFhirInstance.getInstance(); IParser parser = instance.getFhirXmlParser(); String FHIR_OUTPUT = parser.setPrettyPrint( true ).encodeResourceToString( bundle ); if( VERBOSE ) System.out.println( FHIR_OUTPUT ); } }
For building the Bundle request...
package com.windofkeltia.resources; import org.hl7.fhir.r4.model.Bundle; import com.windofkeltia.FhirServerException; import com.windofkeltia.utilities.StringUtilities; public class BundleEntryRequestComponentBuilder { private final Bundle.BundleEntryRequestComponent request = new Bundle.BundleEntryRequestComponent(); private final String method; private final String url; private final String ifNoneMatch; private final String ifModifiedSince; private final String ifMatch; private final String ifNoneExist; public Bundle.BundleEntryRequestComponent get() throws FhirServerException { request.setMethod( CodesAndSystems.httpVerb( method ) ); request.setUrl( url ); if( !StringUtilities.isEmpty( ifNoneMatch ) ) request.getIfNoneMatch(); if( !StringUtilities.isEmpty( ifModifiedSince ) ) request.getIfModifiedSince(); if( !StringUtilities.isEmpty( ifMatch ) ) request.getIfMatch(); if( !StringUtilities.isEmpty( ifNoneExist ) ) request.getIfNoneExist(); return request; } protected BundleEntryRequestComponentBuilder( Builder builder ) { method = builder.method; url = builder.url; ifNoneMatch = builder.ifNoneMatch; ifModifiedSince = builder.ifModifiedSince; ifMatch = builder.ifMatch; ifNoneExist = builder.ifNoneExist; } public static class Builder { private String method; private String url; private String ifNoneMatch; private String ifModifiedSince; private String ifMatch; private String ifNoneExist; public Builder method ( final String METHOD ) { method = METHOD; return this; } public Builder url ( final String URL ) { url = URL; return this; } public Builder ifNoneMatch ( final String IFNONEMATCH ) { ifNoneMatch = IFNONEMATCH; return this; } public Builder ifModifiedSince( final String IFMODIFIEDSINCE ) { ifModifiedSince = IFMODIFIEDSINCE; return this; } public Builder ifMatch ( final String IFMATCH ) { ifMatch = IFMATCH; return this; } public Builder ifNoneExist ( final String IFNONEEXIST ) { ifNoneExist = IFNONEEXIST; return this; } public BundleEntryRequestComponentBuilder build() { return new BundleEntryRequestComponentBuilder( this ); } } }
For building the Bundle entry component...
public class BundleEntryComponentBuilder { private final Bundle.BundleEntryComponent entry = new Bundle.BundleEntryComponent(); private final String link; private final String fullUrl; private final Resource resource; private final String search; private final Bundle.BundleEntryRequestComponent request; private final Bundle.BundleEntryResponseComponent response; @Override public Bundle.BundleEntryComponent get() throws FhirServerException { if( !StringUtilities.isEmpty( fullUrl ) ) entry.setFullUrl( fullUrl ); if( nonNull( resource ) ) entry.setResource( resource ); if( !StringUtilities.isEmpty( search ) ) { /* Bundle.SearchEntryMode.MATCH if the thing matched or * Bundle.SearchEntryMode.INCLUDE if added because referenced by the thing matched. * Bundle.entry.search.score of 1L means "most relevant." */ Bundle.BundleEntrySearchComponent searchComponent = new Bundle.BundleEntrySearchComponent(); searchComponent.setMode( CodesAndSystems.searchMode( search ) ); searchComponent.setScore( 1L ); entry.setSearch( searchComponent ); } if( nonNull( request ) ) entry.setRequest( request ); if( nonNull( response ) ) entry.setResponse( response ); return entry; } ... }
FHIR_OUTPUT is something like this. This isn't much of a bundle, but we're getting there.
<Bundle xmlns="http://hl7.org/fhir"> <identifier> <use value="?"/> <type> <coding> <userSelected value="false"/> </coding> </type> <value value="665892"/> </identifier> <type value="searchset"/> <entry> <fullUrl value="this is a URL"/> <resource> <Patient xmlns="http://hl7.org/fhir"> <identifier> <use value="?"/> <type> <coding> <userSelected value="false"/> </coding> </type> <value value="665892"/> </identifier> <active value="true"/> <name> <use value="usual"/> <family value="Munster"/> <given value="Lily"/> </name> <gender value="female"/> <birthDate value="1827-03-31"/> <deceasedBoolean value="false"/> <address> <use value="home"/> <type value="postal"/> <line value="1313 Mockingbird Lane"/> <city value="Mockingbird Heights"/> <state value="CA"/> <postalCode value="90210"/> <country value="US"/> </address> </Patient> </resource> <search> <mode value="match"/> <score value="1"/> </search> <request> <method value="GET"/> <url value="this is the request URL"/> </request> </entry> </Bundle>
This is far form perfect and it's got a few problems, but it's a useful introduction to how it works using HAPI FHIR.
package com.windofkeltia.processor.helpers; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Patient; import com.windofkeltia.fhir.FhirAddress; import com.windofkeltia.fhir.FhirDate; import com.windofkeltia.fhir.FhirLanguage; import com.windofkeltia.fhir.FhirName; import com.windofkeltia.fhir.FhirPatient; import com.windofkeltia.fhir.FhirTelecom; import com.windofkeltia.utilities.StringUtilities; public class PatientExtractor { private final Patient patient; public String patient_date_of_birth; public String patient_death_flag; public String patient_death_timestamp; public String patient_marital_status; public String patient_language; public String full_name; public String patient_telecom; public String patient_address; public String patient_ssn; public String patient_race; public String patient_ethnicity; public String patient_religion; public PatientExtractor( IBaseResource resource ) { patient = ( Patient ) resource; } public void extract( final String[] paths ) { for( String path : paths ) { switch( path ) { case "Patient.birthDate" : patient_date_of_birth = patient.getBirthDateElement().getValueAsString(); break; case "Patient.deceasedBoolean" : patient_death_flag = String.valueOf( patient.getDeceasedBooleanType().booleanValue() ); break; case "Patient.deceasedDateTime" : patient_death_flag = new FhirDate( patient.getDeceasedDateTimeType().getValue() ).toString(); break; case "Patient.maritalStatus" : patient_marital_status = patient.getMaritalStatus().getCodingFirstRep().getCode(); break; case "Patient.communication.language" : String language = new FhirLanguage( patient.getCommunicationFirstRep() ).getCode(); if( !StringUtilities.isEmpty( language ) ) patient_language = language; break; case "Patient.name" : FhirName name = new FhirName( patient.getNameFirstRep() ); if( !StringUtilities.isEmpty( name.full ) ) full_name = name.full; break; case "Patient.telecom" : FhirTelecom telecom = new FhirTelecom( patient.getTelecomFirstRep() ); patient_telecom = telecom.value; break; case "Patient.address" : FhirAddress address = new FhirAddress( patient.getAddressFirstRep() ); patient_address = address.toString(); break; case "Patient.ssn" : if( !StringUtilities.isEmpty( new FhirPatient( patient ).getSsn() ) ) patient_ssn = ssn; break; case "Patient.race" : String race = new FhirPatient( patient ).getRace(); if( !StringUtilities.isEmpty( race ) ) patient_race = race; break; case "Patient.ethnicity" : String ethnicity = new FhirPatient( patient ).getEthnicity(); if( !StringUtilities.isEmpty( ethnicity ) ) patient_ethnicity = ethnicity; break; case "Patient.religion" : String religion = new FhirPatient( patient ).getReligion(); if( !StringUtilities.isEmpty( religion ) ) patient_religion = religion; break; default : break; } } } }
package com.windofkeltia.fhir; import java.util.List; import static java.util.Objects.isNull; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Resource; public class FhirPatient { private Patient patient; public FhirPatient( Patient patient ) { this.patient = patient; } private static final String SSN_CODING_URL = "http://terminology.hl7.org/CodeSystem/v2-0203"; private static final String SSN_CODING_VALUE = "SS"; private static final String SSN_SYSTEM_URL = "http://hl7.org/fhir/sid/us-ssn"; /** * <identifier> * <type> * <coding> * <system value="http://terminology.hl7.org/CodeSystem/v2-0203" /> SSN_CODING_URL * <code value="SS" /> SSN_CODING_VALUE * </coding> * </type> * <system value="http://hl7.org/fhir/sid/us-ssn" /> SSN_SYSTEM_URL * <value value="500-12-3456" /> * </identifier> */ public String getSsn() { if( isNull( patient ) ) return null; for( Identifier identifier : patient.getIdentifier() ) { if( !identifier.hasType() || !identifier.hasSystem() || !identifier.hasValue() ) continue; CodeableConcept concept = identifier.getType(); if( !concept.hasCoding() ) continue; Coding coding = concept.getCodingFirstRep(); if( !coding.hasSystem() || !coding.hasCode() ) continue; if( !coding.getSystem().equals( SSN_CODING_URL ) ) continue; if( !coding.getCode().equals( SSN_CODING_VALUE ) ) continue; if( !identifier.getSystem().equals( SSN_SYSTEM_URL ) ) continue; return identifier.getValue(); } return null; } private static final String RACE_EXTENSION_URL = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"; private static final String RACE_SYSTEM = "urn:oid:2.16.840.1.113883.6.238"; /** * <extension url="http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"> * <valueCoding> * <system value="urn:oid:2.16.840.1.113883.6.238" /> * <code value="2106-3" /> * <display value="White" /> * </valueCoding> * </extension> */ public String getRace() { if( isNull( patient ) ) return null; Extension race = patient.getExtensionByUrl( RACE_EXTENSION_URL ); if( !race.hasValue() ) return null; Coding coding = ( Coding ) race.getValue(); return coding.getCode() + "(" coding.getDisplay() + ")"; } private static final String ETHNICITY_EXTENSION_URL = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity"; private static final String ETHNICITY_SYSTEM = RACE_SYSTEM; /** * <extension url="http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity"> * <valueCoding> * <system value="urn:oid:2.16.840.1.113883.6.238" /> * <code value="2186-5"/> * <display value="Not Hispanic or Latino" /> * </valueCoding> * </extension> */ public String getEthnicity() { if( isNull( patient ) ) return null; Extension ethnicity = patient.getExtensionByUrl( ETHNICITY_EXTENSION_URL ); if( !ethnicity.hasValue() ) return null; Coding coding = ( Coding ) ethnicity.getValue(); return coding.getCode() + "(" coding.getDisplay() + ")"; } private static final String RELIGION_EXTENSION_URL = "http://hl7.org/fhir/us/core/StructureDefinition/patient-religion"; private static final String RELIGION_SYSTEM = "http://terminology.hl7.org/CodeSystem/v3-ReligiousAffiliation"; /** * <extension url="http://hl7.org/fhir/us/core/StructureDefinition/patient-religion"> * <valueCoding> * <system value="http://terminology.hl7.org/CodeSystem/v3-ReligiousAffiliation" /> * <code value="1027" /> * <display value="Latter Day Saints" /> * </valueCoding> * </extension> */ public String getReligion() { if( isNull( patient ) ) return null; Extension religion = patient.getExtensionByUrl( RELIGION_EXTENSION_URL ); if( !religion.hasValue() ) return null; Coding coding = ( Coding ) religion.getValue(); return coding.getCode() + "(" coding.getDisplay() + ")"; } }
I'd have thought some Crayola® brighter than I would drop such breadcrumbs as these, but I guess I'm the first. I won't explain exactly what I'm doing here because if you find it useful, it's because you need it and already grok the problem, then went googling to find this page.
In some DataTypes I "show my work," that is, fudging around until I figure out what to do. I hope this isn't too confusing. My production code doesn't do that, obviously.
Basically, I have discovered that neither period nor range work nor are particularly useful. I'll revisit this down the road if my customers need those representations.
package com.windofkeltia.hapi.fhir; import java.time.LocalDate; import java.time.Period; import java.time.ZoneId; import java.util.Date; import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.FixMethodOrder; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runners.MethodSorters; import ca.uhn.fhir.model.api.IElement; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r5.model.Age; import org.hl7.fhir.r5.model.AllergyIntolerance; import org.hl7.fhir.r5.model.Base; import org.hl7.fhir.r5.model.DateTimeType; import org.hl7.fhir.r5.model.Range; import org.hl7.fhir.r5.utils.FHIRPathEngine; import com.windofkeltia.utilities.TestUtilities; @FixMethodOrder( MethodSorters.JVM ) public class AllergyIntoleranceTest { @Rule public TestName name = new TestName(); @After public void tearDown() { } @Before public void setUp() { TestUtilities.setUp( name ); } @BeforeClass public static void setUpClass() { context = FhirContext.forR5(); parser = context.newXmlParser().setPrettyPrint( true ); validation = new DefaultProfileValidationSupport( context ); worker = new HapiWorkerContext( context, validation ); fhirEngine = new FHIRPathEngine( worker ); } private static FhirContext context; private static IParser parser; private static DefaultProfileValidationSupport validation; private static IWorkerContext worker; private static FHIRPathEngine fhirEngine; private AllergyIntolerance allergy; private List< Base > values; //<editor-fold desc="Tiny allergy-intolerance bundle..."> private static final String ALLERGYINTOLERANCE_1 = "" + "<AllergyIntolerance xmlns=\"http://hl7.org/fhir\">\n" + " <identifier value=\"example\">\n" + " <system value=\"http://acme.com/ids/patients/risks\"/>\n" + " <value value=\"49476534\"/>\n" + " </identifier>\n" + " <recordedDate value=\"2014-10-09T14:58:00+11:00\"/>\n" + " <recorder>\n" + " <reference value=\"Practitioner/example\"/>\n" + " </recorder>\n" + " <patient>\n" + " <reference value=\"Patient/example\"/>\n" + " </patient>\n" + " <substance>\n" + " <coding>\n" + " <system value=\"http://snomed.info/sct\"/>\n" + " <code value=\"227493005\"/>\n" + " <display value=\"Cashew nuts\"/>\n" + " </coding>\n" + " </substance>\n" + " <status value=\"confirmed\"/>\n" + " <criticality value=\"low\"/>\n" + " <type value=\"allergy\"/>\n" + " <code>\n" + " <coding>\n" + " <system value=\"http://snomed.info/sct\"/>\n" + " <code value=\"387207008\"/>\n" + " <display value=\"Ibuprofen\"/>\n" + " </coding>\n" + " </code>\n" + " <category value=\"food\"/>\n"; private static final String ALLERGYINTOLERANCE_2 = "" + " <lastOccurence value=\"2012-06\"/>\n" + " <reaction>\n" + " <substance>\n" + " <coding>\n" + " <system value=\"http://www.nlm.nih.gov/research/umls/rxnorm\"/>\n" + " <code value=\"C3214954\"/>\n" + " <display value=\"cashew nut allergenic extract Injectable Product\"/>\n" + " </coding>\n" + " </substance>\n" + " <manifestation>\n" + " <coding>\n" + " <system value=\"http://snomed.info/sct\"/>\n" + " <code value=\"39579001\"/>\n" + " <display value=\"Anaphylactic reaction\"/>\n" + " </coding>\n" + " </manifestation>\n" + " <description value=\"Challenge Protocol. Severe Reaction to 1/8 cashew. Epinephrine administered\"/>\n" + " <onset value=\"2012-06-12\"/>\n" + " <severity value=\"severe\"/>\n" + " </reaction>\n" + " <reaction>\n" + " <certainty value=\"likely\"/>\n" + " <manifestation>\n" + " <coding>\n" + " <system value=\"http://snomed.info/sct\"/>\n" + " <code value=\"64305001\"/>\n" + " <display value=\"Urticaria\"/>\n" + " </coding>\n" + " </manifestation>\n" + " <onset value=\"2004\"/>\n" + " <severity value=\"moderate\"/>\n" + " </reaction>\n" + "</AllergyIntolerance>\n"; //</editor-fold> private static final String FHIRPATH = "AllergyIntolerance.onset"; @Test public void testFhirPath_x_datetime() { final String ONSET_DATETIME = " <onsetDateTime value=\"2012-06-12\" />\n"; String ALLERGYINTOLERANCE = ALLERGYINTOLERANCE_1 + ONSET_DATETIME + ALLERGYINTOLERANCE_2; allergy = parser.parseResource( AllergyIntolerance.class, ALLERGYINTOLERANCE ); values = fhirEngine.evaluate( allergy, FHIRPATH ); System.out.println( yields( ONSET_DATETIME ) ); System.out.println( " " + ( ( DateTimeType ) values.get( 0 ) ).asStringValue() ); // Output: 2012-06-12 } @Test public void testFhirPath_x_age() { final String ONSET_AGE = "" + " <onsetAge>\n" + " <value value=\"89\" />\n" + " </onsetAge>\n"; String ALLERGYINTOLERANCE = ALLERGYINTOLERANCE_1 + ONSET_AGE + ALLERGYINTOLERANCE_2; allergy = parser.parseResource( AllergyIntolerance.class, ALLERGYINTOLERANCE ); values = fhirEngine.evaluate( allergy, FHIRPATH ); Age age = ( Age ) values.get( 0 ); System.out.println( yields( ONSET_AGE ) ); System.out.println( " " + age.getValue() ); // Output: 89 } @Test public void testFhirPath_x_period() { final String ONSET_PERIOD = "" + " <onsetPeriod>\n" + " <start value=\"2012-06-12\" />\n" + " <end value=\"2012-07-01\" />\n" + " </onsetPeriod>\n"; String ALLERGYINTOLERANCE = ALLERGYINTOLERANCE_1 + ONSET_PERIOD + ALLERGYINTOLERANCE_2; allergy = parser.parseResource( AllergyIntolerance.class, ALLERGYINTOLERANCE ); values = fhirEngine.evaluate( allergy, FHIRPATH ); org.hl7.fhir.r5.model.Period fhirPeriod = ( org.hl7.fhir.r5.model.Period ) values.get( 0 ); String start = fhirPeriod.getStart().toString(); String end = fhirPeriod.getEnd().toString(); Period period = Period.between( toLocalDate( fhirPeriod.getStart() ), toLocalDate( fhirPeriod.getEnd() ) ); System.out.println( yields( ONSET_PERIOD ) ); System.out.println( " " + period ); // Output: P19D } private LocalDate toLocalDate( Date date ) { return date.toInstant().atZone( ZoneId.systemDefault() ).toLocalDate(); } @Test public void testFhirPath_x_range() { /* I've tried doing the XML differently (subsuming a value element * and even adding a units element under each of low and high elements), * --a little like what ended up working for Age, but the HAPI FHIR * parser throws an exception for everything but this representation: */ final String ONSET_RANGE = "" + " <onsetRange>\n" + " <low value=\"2012-06-12\" />\n" + " <high value=\"2012-07-01\" />\n" + " </onsetRange>\n"; String ALLERGYINTOLERANCE = ALLERGYINTOLERANCE_1 + ONSET_RANGE + ALLERGYINTOLERANCE_2; allergy = parser.parseResource( AllergyIntolerance.class, ALLERGYINTOLERANCE ); values = fhirEngine.evaluate( allergy, FHIRPATH ); /* No matter, values has depth (type Range), but nulls throughout * every field. Alas. */ Base base = values.get( 0 ); IElement r = ( IElement ) base; Range range = ( Range ) r; String low = null, high = null; if( range.hasLow() ) low = String.valueOf( range.getLow() ); if( range.hasHigh() ) high = String.valueOf( range.getHigh() ); System.out.println( yields( ONSET_RANGE ) ); System.out.println( " " + "[ " + low + " .. " + high + " ]" ); // Output: [ null .. null ] } @Test public void testFhirPath_x_string() { final String ONSET_STRING = " <onsetString value=\"2012-06-12\" />\n"; String ALLERGYINTOLERANCE = ALLERGYINTOLERANCE_1 + ONSET_STRING + ALLERGYINTOLERANCE_2; allergy = parser.parseResource( AllergyIntolerance.class, ALLERGYINTOLERANCE ); values = fhirEngine.evaluate( allergy, FHIRPATH ); System.out.println( yields( ONSET_STRING ) ); System.out.println( values.get( 0 ) ); // Output: 2012-06-12 } private String yields( final String ONSET_XML ) { return ONSET_XML.substring( 0, ONSET_XML.length()-1 ) + ':'; } }
For your gee-whiz collection...
You cannot parse anything "smaller" than a DomainResource. For example, if you wanted nothing more than an extension, identifier, name, address, telecom, etc., you cannot create it in a string and generate the corresponding HAPI FHIR resource using the parser. You can only accomplish it by embedding the little resource inside a DomainResource.
For example, I just need an Extension. I find I have to do this, then use the extension out of it:
<Basic xmlns="http://hl7.org/fhir"> <extension url="https://acme.io/fhir/extensions/acme-metadata"> <valueString value="665892"/> </extension> </Basic>
For example, I cannot pass a string containing merely the highlighted lines above. I must wrap these lines in the complete Basic resource (or a Patient or an Encounter, etc.).
@Test public void testParsingExtension() { IBaseResource resource = parser.parseResource( string ); Extension extension = ( Encounter ) resource; }
The result is boom!, a NullPointerException, if you don't include the DomainResource wrapper.