HAPI FHIR notes

Russell Bateman
February 2020
last update:





Finally getting around to starting a page of notes on using HAPI-FHIR.

Links


HAPI FHIR: a simple round trip...
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 ) );
  }
}

Output

<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>

How to find HAPI FHIR Javadoc...

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).


On the FHIR parser...

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:

  1. IBaseResource parseResource( String string ) —used here
  2. IBaseResource parseResource( InputStream inputStream )
  3. IBaseResource parseResource( Reader reader )

...and, almost as conveniently, two versions of IParser.encodeResourceXyz():

  1. String encodeResourceToString( IBaseResource resource )
  2. void encodeResourceToWriter( IBaseResource resource, Writer writer )

Best practice for determining if incoming document is XML or JSON...
String document; // the FHIR content

switch( EncodingEnum.detectEncoding( document ) )
{
  case XML :  break;
  case JSON : break;
  case RDF :  break;
}

Best practice for determining what FHIR resource dominates the document...

Assume that we know the document is in XML.

String document; // the FHIR content

IParser       parser   = context.newXmlParser();
IBaseResource resource = parser.parseResource( document );

What happens if there's junk in the FHIR?

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.

Enhancing ugly XML formatting like <element>(no text here)</element> instead of <element />

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>

Generating Identifier constructs...

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 );

Patients, Identifiers and Extensions

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;
    }
  }

CodeableConcepts and Extensions

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 );
  }
}

Updating HAPI FHIR versions

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:

  1. Ensure that every vestige of 5.1.0 get eradicated (from pom.xml, any direct references to JARs, etc.).
  2. Bounce IntelliJ IDEA by File → Invalidate Caches/Restart.

HAPI FHIR FHIRPath

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 FHIR validator

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
.
.
.

Some beginning steps...

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.

pom.xml:
<?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>
FhirContextTest.java:
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"
      ]
    }
  ]
}

HAPI FHIR Instance Validation Validation follows documentation outline beginning 11.0 Validation Introduction. The strategy we're looking for is called, "Instance Validator."

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):

Location Error
1. 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. Bundle.entry[0].resource.ofType(Patient).identifier[0].system URI values cannot have whitespace('https://fhir.acme.io/facility/Beverly Hills Clinic')
3. 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. 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

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.

Legend (to above)

  1. 1. For the Bundle to be valid, there must be a <request .../> element.
  2. 2. See the URL, line 47. It has spaces, which is illegal.
  3. 3. Missing <status ... /> element for the Encounter. status has a cardinality of 1..1.
  4. 4. Missing <class ... /> element for the Encounter. class has a cardinality of 1..1.

Let's experiment with validation-chain support...

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() );
    }
  }

Output

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

FHIR validation

I left some nicer bread crumbs on validation here.


Populating a FHIR Patient from scratch

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;
  }
  ...
}

Rolling a Patient into a Bundle

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 );
  }
}

Notes

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>

Extracting data from a Patient

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;
      }
    }
  }
}
FhirPatient.java:
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() + ")";
  }
}

Extracting polymorphic onset[x] data using FHIRPath from an AllergyIntolerance

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 ) + ':';
  }
}