The demo Android app uses the Android FHIR SDK to show a patient registration questionnaire and save them locally. It also syncs data with a FHIR server through the FHIR Info Gateway, which provides access control to FHIR resources based on the authenticated user. Finally, FHIR Data Pipes periodically transforms the data to Parquet files that you can query to perform analytics.
- How to integrate the Android FHIR SDK with the FHIR Info Gateway
- How the FHIR Info Gateway works
- How to implement interfaces for syncing FHIR resources
- How to transform and export FHIR data to query with Spark SQL
The diagram below shows the different components that are involved:
- Java 8 or higher
- Android Studio set up with an Android emulator and an SDK at 31 or higher
- Docker
- Docker-Compose v2 or later
-
Clone the FHIR App Examples repo:
git clone https://github.com/google/fhir-app-examples.git
-
Open Android Studio, select Import Project (Gradle, Eclipse ADT, etc.) and choose the
fhir-app-examples
folder downloaded in the previous step. If this is your first time opening the project, the Gradle Build process should start (and take some time).
-
Clone the FHIR Data Pipes repo:
git clone https://github.com/google/fhir-data-pipes.git
-
Follow the instructions to set up a local test server. At step 2, follow the instructions for a "HAPI source server with Postgres". You can omit the optional step 4.
You now have a HAPI FHIR server that you loaded with synthetic patient data; exactly 79 patients, if it only has the test data.
-
From a terminal, run:
PATIENT_ID1=4765 PATIENT_ID2=4767 curl -X PUT -H "Content-Type: application/json" \ "http://localhost:8091/fhir/List/patient-list-example" \ -d '{ "resourceType": "List", "id": "patient-list-example", "status": "current", "mode": "working", "entry": [ { "item": { "reference": "Patient/'"${PATIENT_ID1}"'" } }, { "item": { "reference": "Patient/'"${PATIENT_ID2}"'" } } ] }'
Note: If you are copy and pasting from OSX, you may need to first paste into a text editor to put all of that in a single line (and removing trailing "\"), before pasting that into shell to run.
This creates a FHIR List on the server with the id
patient-list-example
, which we will use as an access control list. It contains references toPatient/4765
andPatient/4767
which the user is allowed to access. -
Follow the instructions to set up a single-machine analytics pipeline. This Docker image includes the FHIR Pipelines Controller plus a Spark Thrift server where data is ultimately loaded for querying.
-
In a web browser, visit http://localhost:8090 to see the FHIR Pipelines Controller UI.
-
Clone the FHIR Info Gateway repo:
git clone https://github.com/google/fhir-gateway.git
-
Start the Keycloak Identity Provider Server. From the fhir-gateway directory, run:
docker-compose -f docker/keycloak/config-compose.yaml \ up --force-recreate --remove-orphans -d --quiet-pull
The config-compose.yaml sets up a Keycloak instance that can support both a list-based access control and a single-patient based SMART-on-FHIR app (in two separate realms).
The
keycloak-config
image is built using the Dockerfile here. A key component of the Dockerfile is the keycloak_setup.sh. There are two points of interest in this script: the first is this, which creates a client that authenticated users can act as, and here where we create a user that binds thepatient-list-example
value to thepatient_list
claim field that is part of the JWT access token. The default username and password used for the user are from the env file. -
Start the FHIR Info Gateway. From the fhir-gateway directory, run:
docker run --rm --network host \ -e TOKEN_ISSUER="http://localhost:9080/auth/realms/test" \ -e PROXY_TO="http://localhost:8091/fhir" \ -e BACKEND_TYPE="HAPI" \ -e RUN_MODE="DEV" \ -e ACCESS_CHECKER=list \ -e ALLOWED_QUERIES_FILE="resources/hapi_page_url_allowed_queries.json" \ us-docker.pkg.dev/fhir-proxy-build/stable/fhir-access-proxy:latest
This brings up a FHIR Info Gateway, connected to the HAPI FHIR server. The
TOKEN_ISSUER
variable is the IP of the Keycloak IDP from the previous step, and thePROXY_TO
variable is the IP of the FHIR server. As we are running theTOKEN_ISSUER
and FHIR Info Gateway on the same machine (but on different ports), we need to bypass the Proxy's token issuer check by setting the environment variableRUN_MODE
toDEV
.WARNING: Never use
RUN_MODE=DEV
in a production environment.Part of setting up the FHIR Info Gateway is choosing the type of Access Checker to use. This is set using the
ACCESS_CHECKER
environment variable (See here for more detail). In this demo, we will use the default value oflist
, which will use theListAccessChecker
to manage incoming requests. This access-checker uses thepatient_list
ID in the JWT access token to fetch the "List" of patient IDs that the given user has access to. There are some URL requests that we want to bypass the access checker (e.g. URLs with_getpages
in them) and we declare these rules inhapi_page_url_allowed_queries.json
. To make the server use this file, we set the environment variableALLOWED_QUERIES_FILE
.
This example demonstrates several components of Open Health Stack.
The Demo app uses the Structured Data Capture library to render the patient registration and survey forms, and to extract FHIR resources based on the responses. You can see a form by clicking the Add Patient button in the bottom-right of the main screen.
The Demo app also uses the FHIR Engine library to save FHIR resources in the app and sync them with a FHIR server. You can see this when resources sync from the server the first time, or when you register new patients.
-
In Android Studio, with an Android Emulator installed, run the
demo
app by pressing on the Play button on the top bar. This will build the app and open the emulator. -
Once the app finishes building it will launch in the emulator and its logs will be available in the bottom Run tab of Android Studio.
-
In the Emulator, press the Log In button, which will take you to the IDP login screen. Type testuser as the username and testpass as the password.
-
The app will then start the syncing process. You can see this in the logs displayed in the Run tab.
When the Demo app syncs resources with the FHIR server, it is actually communicating
with a FHIR Info Gateway.
It uses the List Access Checker
to determine which Patient resources testuser
has access to, and then fetches the
resources from the actual FHIR server when allowed. The demo app is designed to only
send requests that are expected to succeed, but you can follow the guide to try out the Info
Gateway
for more information.
The FHIR Data Pipes Pipelines Controller facilitates the transformation of data from a FHIR server to Parquet files. In this guide, you use the single machine configuration which also loads the Parquet files into a Spark Thrift server for you.
-
Visit the Pipeline Controller UI at http://localhost:8090.
-
Click on Run Full to generate the Parquet files.
-
Connect to jdbc:hive2://localhost:10001 using a Hive/Spark client.
-
Count the number of patients:
SELECT COUNT(0) FROM default.patient;
-
From the demo app running in the Android Emulator, register a new patient by selecting the New Patient (+) button and complete the registration form.
-
Force the app to sync with the server by tapping the menu button and selecting Sync.
-
Update the Parquet files by visiting the Pipeline Controller UI and clicking Run Incremental.
-
Query the number of patients again:
SELECT COUNT(0) FROM default.patient;
If you have any errors when running the incremental pipeline or it fails to work, try using
sudo chmod -R 755
on the Parquet file directory, default located at
fhir-data-pipes/tree/master/docker/dwh
.
When the app is launched, the first class launched is
FhirApplication
,
as it is a subclass of
Application
and specified in the "android:name"
field in
AndroidManifest.xml
. Part of the
FhirApplication
class instantiates a
ServerConfiguration
.
We pass into the ServerConfiguration
the URL of the FHIR Access Proxy. As we
are running the Proxy and the App from the same machine, we use
10.0.2.2
as a
special alias to the host loopback interface (i.e., 127.0.0.1 on the same
machine). We also pass into the ServerConfiguration
an instance of
Authenticator
for supplying the Proxy the JWT access token;
LoginRepository
is the implementation of Authenticator
we wrote.
Our end-to-end setup uses OAuth 2.0 authorization code flow to retrieve an access token.
After initializing the FhirApplication
class, the next class launched is the
LoginActivity
class, as specified by the intent filters in the AndroidManifest.xml
file. The
LoginActivity
class initializes the
LoginActivityViewModel
class; the LoginActivityViewModel
contains two methods that are called by
LoginActivity
: createIntent
and handleLoginResponse
. The first method
returns an
Intent
that
is bound to the Log In button. The intent is built by first fetching the
Discovery Document
from the Proxy. The URL to the Proxy discovery endpoint is loaded from the
auth_config.json
. When a request to the
Proxy is made to this endpoint, it returns a response that includes the value of
TOKEN_ISSUER
, which is needed to create the login Intent.
When the Log In button is pressed, the Intent opens a webpage to the login
screen with the value of TOKEN_ISSUER
as the base URL, where the user is
prompted to type in their credentials. Once the user logs in, the callback
defined in the getContent
variable in LoginActivity
runs, which takes the
response from the IDP containing an
authorization code,
and passes it to the handleLoginResponse
method. This method abstracts the
exchange of the authorization code for an access token, which is stored in the
App. Any call in the app now made to LoginRepository.getAccessToken
fetches
the stored JWT, and if expired, refreshes the token.
Once the user logs in, the
MainActivity
class is launched, which instantiates the
MainActivityViewModel
class. When MainActivityViewModel
is initialized, it launches an instance of
SyncJob
.
One of the parameters we need to pass in to the SyncJob.poll
method is an
implementation of the
FhirSyncWorker
abstract class, which we provide via the
FhirPeriodicSyncWorker
class.
FhirPeriodicSyncWorker
implements two methods, one of which is
getDownloadWorkManager
. The implementation of that method requires a
DownloadWorkManager
returned, a class that we also have to implement. We have to provide a way for
the SDK to generate the FHIR download requests and handle the FHIR responses
returned, and we do that via the
DownloadWorkManagerImpl
class. This class takes in an initial resource ID to seed the first download
request; this resource ID comes from the value of the patient_list
claim that
is part of the JWT access token now stored on the App.
As we logged in as testuser
, the value of patient_list
will be
patient-list-example
, which we defined in Keycloak. patient-list-example
is
the ID of a List resource on the FHIR server that we first want to fetch. With
FhirPeriodicSyncWorker
and DownloadWorkManagerImpl
instantiated, the
SyncJob.poll
method runs and downloads all resources as specified in the
classes we created.