FHIR Programming using Java and HAPI FHIR Server - Reading FHIR Resources

Introduction

Welcome to another article in my series on FHIR Programming using Java and HAPI FHIR Server. In this tutorial, we will focus on how to read FHIR resources from a HAPI FHIR Server using Java. This article builds upon the knowledge from our previous discussions on creating FHIR resources. If you're unfamiliar with creating resources, I recommend reviewing that article before proceeding.

Reading FHIR resources is a critical operation in healthcare applications. Whether you're developing a patient management system or an EHR, retrieving and displaying stored data efficiently is essential. In this article, I will walk you through the steps to read FHIR resources from a server using Java, ensuring you can access and manipulate healthcare data stored in a FHIR-compliant format.

Understanding FHIR Read Operations

Before diving into the code, let's understand the key concepts that govern how FHIR resources are identified, versioned, and retrieved.

Resource Identity: Logical ID vs Business Identifiers

FHIR resources have two distinct types of identifiers:

Logical ID (Resource.id) - A server-assigned identifier that uniquely identifies a resource within a specific FHIR server:

  • Assigned by the server when the resource is created
  • Used in the resource URL: [base]/Patient/123
  • Only unique within that server - the same logical ID may exist on different servers
  • Should not carry business meaning (it's just a technical identifier)

Business Identifiers (Resource.identifier) - Organization-assigned identifiers that have meaning in the real world:

  • Examples: Medical Record Number (MRN), Social Security Number, Driver's License
  • Portable across systems - the same patient keeps their MRN regardless of which FHIR server stores their data
  • Use system/value pairs for global uniqueness
  • A resource can have multiple business identifiers

When designing integrations, prefer searching by business identifiers rather than logical IDs, as business identifiers remain consistent across different FHIR servers.

Resource Versioning

Every FHIR resource maintains version information in its meta element:

  • versionId - A server-assigned version number that increments with each update. Combined with the logical ID, it creates a unique reference to a specific version: Patient/123/_history/2
  • lastUpdated - Timestamp indicating when the resource was last modified on the server

You can retrieve specific versions or the complete history of a resource:

// Read the current version
Patient current = client.read()
    .resource(Patient.class)
    .withId("123")
    .execute();

// Read a specific version
Patient version2 = client.read()
    .resource(Patient.class)
    .withIdAndVersion("123", "2")
    .execute();

// Get complete history
Bundle history = client.history()
    .onInstance(new IdType("Patient", "123"))
    .returnBundle(Bundle.class)
    .execute();

Bundle Types

When reading multiple resources or search results, FHIR returns them in a Bundle. Understanding bundle types is essential:

  • searchset - Results from a search operation. Includes pagination links and total count.
  • collection - A curated set of resources grouped together (no specific processing semantics).
  • transaction - A set of operations to be performed atomically (all-or-nothing).
  • transaction-response - The server's response to a transaction bundle.
  • batch - Similar to transaction but operations are independent (some may fail while others succeed).
  • batch-response - The server's response to a batch bundle.
  • history - Version history of a resource or set of resources.
  • document - A clinical document with a Composition as the first entry.
  • message - A FHIR message with MessageHeader as the first entry.

Search results always return a searchset bundle with entries containing the matched resources:

Bundle results = client.search()
    .forResource(Patient.class)
    .returnBundle(Bundle.class)
    .execute();

// Bundle type will be "searchset"
System.out.println("Bundle type: " + results.getType());

// Total number of matches (may be more than entries returned due to pagination)
System.out.println("Total matches: " + results.getTotal());

// Pagination links
if (results.getLink(Bundle.LINK_NEXT) != null) {
    System.out.println("More results available");
}

HTTP Content Negotiation

FHIR supports both JSON and XML representations. Clients can request their preferred format using HTTP content negotiation:

  • Accept header - Specifies the desired response format:
    • application/fhir+json - Request JSON format
    • application/fhir+xml - Request XML format
  • _format parameter - Alternative way to specify format in the URL: ?_format=json
  • Content-Type header - Specifies the format of data being sent to the server

In HAPI FHIR, you can configure the preferred format when creating the context or client:

// Set encoding preference on context
ctx.getRestfulClientFactory().setDefaultPreferredFormat("json");

// Or serialize resources in a specific format
String jsonOutput = ctx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient);
String xmlOutput = ctx.newXmlParser().setPrettyPrint(true).encodeResourceToString(patient);

JSON is generally preferred for web applications due to smaller payload size and native JavaScript support, while XML may be required for certain legacy integrations.

Error Handling Patterns

Robust error handling is essential when reading FHIR resources. Common scenarios include:

  • 404 Not Found - Resource doesn't exist (ResourceNotFoundException)
  • 410 Gone - Resource was deleted
  • 403 Forbidden - Insufficient permissions to read the resource
  • 400 Bad Request - Invalid search parameters or malformed request

FHIR servers return an OperationOutcome resource with error details, which can provide valuable diagnostic information.

Prerequisites

Before you start, ensure your environment is ready:

  • Java Development Kit (JDK) is installed and configured.
  • Apache Maven is installed and set up for your project.
  • HAPI FHIR Server is running and accessible.
  • You can find all the code demonstrated in this tutorial on GitHub here

“Of all the great national heroes and statesmen of history Lincoln is the only real giant. Alexander, Frederick the Great, Caesar, Napoleon, Gladstone and even Washington stand in greatness of character, in depth of feeling and in a certain moral power far behind Lincoln. Lincoln was a man of whom a nation has a right to be proud; he was a Christ in miniature, a saint of humanity, whose name will live thousands of years in the leg­ends of future generations. We are still too near to his greatness, and so can hardly appreciate his divine power; but after a few centuries more our posterity will find him considerably bigger than we do. His genius is still too strong and too powerful for the common understanding, just as the sun is too hot when its light beams directly on us” ~ Leo Tolstoy about Abraham Lincoln

Step 1 of 4: Import Required Classes

To begin, we need to import the necessary classes from the HAPI FHIR library. These classes will help us interact with the FHIR server and retrieve the desired resources. Open your `App.java` file and import the following classes:

package com.saravanansubramanian.fhir;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType;

These imports include the core FHIR context and client classes, which are essential for communication with the FHIR server, and the `Patient` and `Bundle` classes for handling patient resources and search results. Note the `ResourceNotFoundException` import for handling errors when resources are not found.

Step 2 of 4: Connect to the FHIR Server

Next, we will establish a connection to the HAPI FHIR Server. This connection will enable us to send requests to the server and retrieve FHIR resources. The following code snippet demonstrates how to create a connection:

public class FhirReadResources {
    public static void main(String[] args) {
        // Replace with your FHIR server base URL
        String fhirServerUrl = "http://hapi.fhir.org/baseR4";

        // Initialize FHIR context and client
        FhirContext ctx = FhirContext.forR4();
        IGenericClient client = ctx.newRestfulGenericClient(fhirServerUrl);

        System.out.println("Successfully connected to FHIR server");
    }
}

This snippet initializes the FHIR context and creates a RESTful client pointing to the HAPI FHIR public test server. You can replace the URL with your own FHIR server's base URL.

Step 3 of 4: Read a Specific Resource

Once connected, we can read a specific FHIR resource by its ID. First, let's search for an existing patient to get a valid ID, then read that specific patient:

// Step 1: Search for any patient to get a valid ID
System.out.println("Step 1: Searching for a Patient to read...");

Bundle searchResult = client.search()
        .forResource(Patient.class)
        .count(1)
        .returnBundle(Bundle.class)
        .execute();

if (searchResult.hasEntry() && !searchResult.getEntry().isEmpty()) {
    Resource resource = searchResult.getEntry().get(0).getResource();

    // Safe type check using instanceof
    if (resource instanceof Patient firstPatient) {
        String patientId = firstPatient.getIdElement().getIdPart();

        System.out.println("Found patient ID: " + patientId);

        // Step 2: Read the specific patient by ID
        System.out.println("Reading Patient by ID...");

        Patient patient = client.read()
                .resource(Patient.class)
                .withId(patientId)
                .execute();

        System.out.println("Patient Details:");
        System.out.println("  ID: " + patient.getId());

        if (patient.hasName() && !patient.getName().isEmpty()) {
            HumanName name = patient.getNameFirstRep();
            String givenNames = name.hasGiven()
                    ? String.join(" ", name.getGiven().stream()
                        .map(StringType::getValue)
                        .collect(java.util.stream.Collectors.toList()))
                    : "";
            System.out.println("  Name: " + name.getFamily() + ", " + givenNames);
        }

        System.out.println("  Gender: " + patient.getGender());
        System.out.println("  Birth Date: " + patient.getBirthDate());
        System.out.println("  Active: " + patient.getActive());
    }
} else {
    System.out.println("No patients found on the server.");
}

This code first searches for any existing patient to get a valid ID, then retrieves that specific patient by ID. It uses the `instanceof` pattern matching for safe type checking and includes null-safe access patterns using `hasName()` and `hasGiven()` checks.

Step 4 of 4: Search for Resources

In many cases, you may want to search for resources based on specific criteria rather than by ID. The following example demonstrates how to search for all patients with a given family name:

// Search for patients by family name
Bundle results = client.search()
    .forResource(Patient.class)
    .where(Patient.FAMILY.matches().value("Smith"))
    .returnBundle(Bundle.class)
    .execute();

// Print the number of results
System.out.println("Found " + results.getTotal() + " patients with the family name 'Smith'");

// Iterate over search results and print each patient's information
for (Bundle.BundleEntryComponent entry : results.getEntry()) {
    Resource resource = entry.getResource();

    // Safe type check using instanceof with pattern matching
    if (resource instanceof Patient searchedPatient) {
        System.out.println("Patient ID: " + searchedPatient.getIdElement().getIdPart());

        if (searchedPatient.hasName()) {
            HumanName name = searchedPatient.getNameFirstRep();
            String givenNames = name.hasGiven()
                ? String.join(" ", name.getGiven().stream().map(StringType::getValue).toList())
                : "";
            System.out.println("Name: " + name.getFamily() + ", " + givenNames);
        }

        if (searchedPatient.hasIdentifier()) {
            System.out.println("First Identifier: " +
                searchedPatient.getIdentifierFirstRep().getSystem() + " | " +
                searchedPatient.getIdentifierFirstRep().getValue());
        }

        System.out.println("-----------------------------------");
    }
}

This code searches for all patients with the family name "Smith" and prints each result, including the patient's ID, name, and identifier. This search functionality is crucial for retrieving specific sets of data based on various parameters.

Error Handling

When reading resources, it's important to handle cases where the requested resource doesn't exist. The HAPI FHIR library throws a `ResourceNotFoundException` when a resource cannot be found:

// Error handling for non-existent resources
System.out.println("Handling errors when reading non-existent resources...");

try {
    Patient nonExistent = client.read()
            .resource(Patient.class)
            .withId("non-existent-id-12345")
            .execute();
} catch (ResourceNotFoundException e) {
    System.out.println("Expected error caught: " + e.getMessage());
    System.out.println("  The specified patient could not be found (404 Not Found).");
}

This pattern ensures your application can gracefully handle cases where requested resources don't exist on the server.

Conclusion

In this article, we explored the steps to read FHIR resources using Java and the HAPI FHIR library. We covered how to establish a connection to a FHIR server, retrieve specific resources by ID, and search for resources based on criteria. These capabilities are essential for accessing and displaying healthcare data within your applications.

In the next tutorial in this series, we will learn how to create FHIR resources, enabling you to add new healthcare data to your FHIR server.