A brand new FHIR (Fast Healthcare Interoperability Resources) adapter bridges the gap between SPICE and this widely used data format. This adapter acts like a translator, effortlessly converting SPICE data into FHIR format.
Centralized Data Hub:
The converted data is then mapped to corresponding FHIR resources (think data fields) and pushed to a central FHIR database. This creates a central repository for your healthcare information.
Reaching New Horizons:
The stored FHIR data isn't locked away. It can be easily transferred to DHIS2 (District Health Information System 2) systems used in Africa and Bangladesh. This fosters broader data exchange and accessibility across regions.
Multilingual Capability:
SPICE now supports multi-language translation for the user interface! This means you can experience SPICE in your preferred language, making it easier than ever to navigate and use the app.
Tech stack
Git
Java 17.x
Apache Maven 3.8.x
Docker 20.10.xx
Docker-compose 1.29.x
RabbitMQ
HAPI-FHIR Starter Project
Installation
To bring the fhir-adapter-service up and running, there are few prerequisite that has to be done. You can either follow the commands or access the official documentation by clicking the hyperlink.
To install git in Ubuntu run the following command or click Git Official site.
In order to transform the spice object into a FHIR object, it needs to be transmitted to the FHIR adapter. To ensure asynchronous communication, we're implementing a queue service. We'll employ RabbitMQ for this purpose, leveraging its open-source capabilities. Run the application and configure the environment settings in the adapter service's configuration file. Install RabbitMq by following the steps give in RabbitMQ Official site -Download.
HAPI-FHIR Starter Project
After the adapter creates the FHIR request, it proceeds to initiate a service call to the HAPI-FHIR Starter Project for saving the FHIR resource. This project is open-source, and its setup can be completed by referring to the documentation provided within the HAPI FHIR JPA Server.
Note: Once you have executed the script mentioned above, please restart the application.
Setup
Clone the fhir-service repository.
$gitclone<<Needtobeaddedlater>>
Contribution guidelines
To run the application, you should pass the necessary configuration via environment properties. To achieve this, create a .env file and pass your own values for the following properties.
Note: Please paste the .env file inside the specified directory.
<home path>/fhir-service/
.envfile
PROJECT_PATH=/home/ubuntu/Documents/fhir-serviceHAPI_FHIR_SERVER_PROTOCOL=httpHAPI_FHIR_SERVER_HOSTNAME=example.comHAPI_FHIR_SERVER_PORT=1234HAPI_FHIR_SERVER_URI=/fhir/HAPI_FHIR_SERVER_BASE_URL="${HAPI_FHIR_SERVER_PROTOCOL}://${HAPI_FHIR_SERVER_HOSTNAME}:${HAPI_FHIR_SERVER_PORT}${HAPI_FHIR_SERVER_URI}"
DRIVER_CLASS_NAME=org.postgresql.DriverDATABASE_URL=jdbc:postgresql://example-db-host:5432/example-db?serverTimezone=UTC&stringtype=unspecifiedDATABASE_USERNAME=postgresDATABASE_PASSWORD=passwordDATABASE_NAME=example-dbFHIR_LOG_FILENAME='./log/FHIR_ApplicationLog.log'FHIR_LOG_FILENAME_PATTERN='./log/FHIR_ApplicationLog.%d{yyyy-MM-dd}.log.gz'FHIR_LOG_CONSOLE_PATTERN='%white(%d{ISO8601}) %highlight(%-5level) [%yellow(%t)] : %msg%n%throwable'FHIR_LOG_FILE_PATTERN='%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n'FHIR_LOG_MAX_HISTORY=30FHIR_LOG_TOTAL_SIZE_CAP=3GBFHIR_PROTOCOL='http'FHIR_LISTENER_PORT=8141FHIR_LISTENER_DOCKER_SERVICE_NAME='fhir-listener-service'FHIR_LISTENER_APPLICATION_NAME=fhir-listener-serviceFHIR_LISTENER_CONTEXT_PATH='/fhir-listener-service'FHIR_LISTENER_SERVICE_BASE_URL=${FHIR_PROTOCOL}://${FHIR_LISTENER_DOCKER_SERVICE_NAME}:${FHIR_LISTENER_PORT}FHIR_ADAPTER_PORT=8142FHIR_ADAPTER_DOCKER_SERVICE_NAME='fhir-adapter-service'FHIR_ADAPTER_APPLICATION_NAME=fhir-adapter-serviceFHIR_ADAPTER_CONTEXT_PATH='/fhir-adapter-service'FHIR_ADAPTER_SERVICE_BASE_URL=${FHIR_PROTOCOL}://${FHIR_ADAPTER_DOCKER_SERVICE_NAME}:${FHIR_ADAPTER_PORT}FHIR_USER_PORT=8143FHIR_USER_DOCKER_SERVICE_NAME='fhir-user-service'FHIR_USER_APPLICATION_NAME=fhir-user-serviceFHIR_USER_CONTEXT_PATH='/fhir-user-service'FHIR_USER_SERVICE_BASE_URL=${FHIR_PROTOCOL}://${FHIR_USER_DOCKER_SERVICE_NAME}:${FHIR_USER_PORT}FHIR_AUTH_PORT=8144FHIR_AUTH_DOCKER_SERVICE_NAME='fhir-auth-service'FHIR_AUTH_APPLICATION_NAME=fhir-auth-serviceFHIR_AUTH_CONTEXT_PATH='/fhir-auth-service'FHIR_AUTH_SERVICE_BASE_URL=${FHIR_PROTOCOL}://${FHIR_AUTH_DOCKER_SERVICE_NAME}:${FHIR_AUTH_PORT}FHIR_PUBLIC_KEY_FILE_NAME=fhir_public_key.derFHIR_PRIVATE_KEY_FILE_NAME=fhir_private_key.derFHIR_ACCESS_KEY_ID=accesskeyFHIR_SECRET_ACCESS_KEY=secretkeyRABBITMQ_HOSTNAME=example-rabbitmq-hostRABBITMQ_MANAGEMENT_PORT=9999RABBITMQ_AMQP_PORT=8888RABBITMQ_USER_NAME=guestRABBITMQ_USER_PASSWORD=guestRABBITMQ_QUEUE_NAME=spice_rabbitmq_queueRABBITMQ_EXCHANGE_NAME=spice_rabbitmq_exchangeRABBITMQ_ROUTING_KEY=spice_rabitmq_routing_keyPASSWORD: Password parameter key for security config and the value must be password.SOURCE_DATABASE_URL=jdbc:postgresql://example-db-host:5432/example-db?serverTimezone=UTC&stringtype=unspecifiedSOURCE_DATABASE_USERNAME=postgresSOURCE_DATABASE_PASSWORD=passwordSOURCE_DATABASE_NAME=example-db
Note: The values for the environmental variables should be changed based on the chosen service.
.env
The .env file is used to store environment variables for the project. These variables are used to configure the application and contain sensitive information such as passwords, API keys, and other credentials.
Please note that the .env file should never be committed to version control, as it contains sensitive information that should not be shared publicly. Instead, you should add the .env file to your .gitignore file to ensure that it is not accidentally committed.
To use the application, you will need to create a .env file in the root directory of the project and add the necessary environment variables. You can refer to the above file for an example of the required variables and their format.
The values provided in the instructions are for demonstration purposes only and will not work as-is. You will need to replace them with actual values that are specific to your environment.
Note: After checking out the project, please ensure that you update the relevant properties and values in env.example, and then rename it to .env.
.env description
PROJECT_PATH: Specifies the file path to the project's backend directory on the server.
HAPI_FHIR_SERVER_PROTOCOL: Defines the protocol used for communication with the HAPI FHIR server, set to HTTP.
HAPI_FHIR_SERVER_HOSTNAME: Indicates the IP address of the host where the HAPI FHIR server is running.
HAPI_FHIR_SERVER_PORT: Specifies the port number on which the HAPI FHIR server is listening.
HAPI_FHIR_SERVER_URI: Represents the URI path for accessing the FHIR server resources.
HAPI_FHIR_SERVER_BASE_URL: Constructs the base URL for the HAPI FHIR server using the protocol, hostname, port, and URI.
DRIVER_CLASS_NAME: Specifies the Java driver class name for connecting to the PostgreSQL database.
DATABASE_URL: Contains the JDBC URL for connecting to the PostgreSQL database with timezone and string type specified.
DATABASE_USERNAME: Username used for accessing the PostgreSQL database.
DATABASE_PASSWORD: Password for the PostgreSQL user specified.
DATABASE_NAME: Name of the PostgreSQL database used by the project.
FHIR_LOG_FILENAME: File path for storing the log file related to the FHIR application.
FHIR_LOG_FILENAME_PATTERN: Pattern for rotating and compressing the log files based on date.
FHIR_LOG_CONSOLE_PATTERN: Pattern for formatting the log messages displayed on the console.
FHIR_LOG_FILE_PATTERN: Pattern for formatting the log messages written to the log file.
FHIR_LOG_MAX_HISTORY: Maximum number of historical log files to retain.
FHIR_LOG_TOTAL_SIZE_CAP: Maximum total size of log files allowed, set to 3 gigabytes.
FHIR_PROTOCOL: Protocol used for communication with FHIR services, set to HTTP.
FHIR_LISTENER_PORT: Port number on which the FHIR listener service is running.
FHIR_LISTENER_DOCKER_SERVICE_NAME: Name of the Docker service hosting the FHIR listener.
FHIR_LISTENER_APPLICATION_NAME: Name of the FHIR listener application.
FHIR_LISTENER_CONTEXT_PATH: Context path for accessing the FHIR listener service.
FHIR_LISTENER_SERVICE_BASE_URL: Base URL constructed using the protocol, Docker service name, and port for the FHIR listener service.
FHIR_ADAPTER_PORT: Port number on which the FHIR adapter service is running.
FHIR_ADAPTER_DOCKER_SERVICE_NAME: Name of the Docker service hosting the FHIR adapter.
FHIR_ADAPTER_APPLICATION_NAME: Name of the FHIR adapter application.
FHIR_ADAPTER_CONTEXT_PATH: Context path for accessing the FHIR adapter service.
FHIR_ADAPTER_SERVICE_BASE_URL: Base URL constructed using the protocol, Docker service name, and port for the FHIR adapter service.
FHIR_USER_PORT: Port number on which the FHIR user service is running.
FHIR_USER_DOCKER_SERVICE_NAME: Name of the Docker service hosting the FHIR user service.
FHIR_USER_APPLICATION_NAME: Name of the FHIR user application.
FHIR_USER_CONTEXT_PATH: Context path for accessing the FHIR user service.
FHIR_USER_SERVICE_BASE_URL: Base URL constructed using the protocol, Docker service name, and port for the FHIR user service.
FHIR_AUTH_PORT: Port number on which the FHIR authentication service is running.
FHIR_AUTH_DOCKER_SERVICE_NAME: Name of the Docker service hosting the FHIR authentication service.
FHIR_AUTH_APPLICATION_NAME: Name of the FHIR authentication application.
FHIR_AUTH_CONTEXT_PATH: Context path for accessing the FHIR authentication service.
FHIR_AUTH_SERVICE_BASE_URL: Base URL constructed using the protocol, Docker service name, and port for the FHIR authentication service.
FHIR_PUBLIC_KEY_FILE_NAME: Filename for the public key used in FHIR service.
FHIR_PRIVATE_KEY_FILE_NAME: Filename for the private key used in FHIR service.
FHIR_ACCESS_KEY_ID: Access key ID used for authentication.
FHIR_SECRET_ACCESS_KEY: Secret access key used for authentication.
CLIENT_REGISTRY_ENABLED_COUNTRY: Number representing the enabled country in the client registry.
RABBITMQ_HOSTNAME: IP address of the RabbitMQ server.
RABBITMQ_MANAGEMENT_PORT: Management port for RabbitMQ.
RABBITMQ_AMQP_PORT: AMQP port for RabbitMQ.
RABBITMQ_USER_NAME: Username for accessing RabbitMQ.
RABBITMQ_USER_PASSWORD: Password for accessing RabbitMQ.
RABBITMQ_QUEUE_NAME: Name of the queue used in RabbitMQ.
RABBITMQ_EXCHANGE_NAME: Name of the exchange used in RabbitMQ.
RABBITMQ_ROUTING_KEY: Routing key used in RabbitMQ.
PASSWORD: Password parameter key for security config and the value must be password.
Note The Below properties are used for the sourceDatabase(SPICE-SERVER Database details) configuration for migration.
SOURCE_DRIVER_CLASS_NAME: Specifies the Java driver class name for connecting to the PostgreSQL database.
SOURCE_DATABASE_URL: Contains the JDBC URL for connecting to the PostgreSQL database with timezone and string type specified.
SOURCE_DATABASE_USERNAME: Username used for accessing the PostgreSQL database.
SOURCE_DATABASE_PASSWORD: Password for the PostgreSQL user specified.
SOURCE_DATABASE_NAME: Name of the PostgreSQL database used by the project.
Deployment
run the following commands in the below path
Build a clean-install using maven /fhir-service
$mvncleaninstall
Build docker images by the following command
$docker-composebuild
Once the images are built, run the docker containers by the following docker command
$docker-composeup
Validation
Once the deployment of the application is successful, you can confirm the connectivity of the Back-end by logging into the application. To accomplish this, you may choose any API Testing tool. In this particular case, the Postman API Platform was utilized as an example.
Endpoint
POST {{ipAdd}}:8762/fhir-auth-service/session
Request Body
This endpoint allows user to sign-in into the application. The request body should be in the form-data format and include the following fields:
username: fhiradmin@mtdlabs.com
password: fhirAdmin@123
The response contains Authorization. You must use this value in the subsequent requests.
Note: The credentials displayed in the tables are for demonstration purposes only and should not be used in a production environment. If you need to remove, modify, or add new user credentials, you can create a new script file containing the necessary DDL or DML queries in the below specified location. It's important to note that attempting to update the existing script file may result in a checksum error.
The authorization token primarily serves the purpose of authenticating user CRUD operations and generating API key pairs only. Communication between microservices is exclusively authenticated only through the API key pair. This key pair utilized for microservices communication must be generated for a user with administrative privileges. Then the same should then be configured within the .env file.
Endpoint
POST {{ipAdd}}/fhir-user-service/api-key-managers/generate
Request Body
{ "id": 1 }
Request Headers
Authorization: {{authToken}}
The payload must include an ID with an admin role. Set the authorization value to the authToken received from the login response headers.
Important:
Since adapter services are not utilized by users but by the FHIR services themselves, it is impossible for an application service to authenticate using user credentials. To address this issue, a new security strategy involving API key pairs has been implemented. Consequently, when the application itself makes service calls, it must attach API keys to the request headers. These API keys need to be manually generated by an administrator. The following steps outline the process:
During the initial deployment of the application, there will be no default API keys available for service calls.
After the first deployment, log in using user credentials to obtain an authentication bearer token.
Utilize this token to create an API key pair, ensuring it is created under an admin user.
Set the received key pair into the .env file.
Finally, redeploy the application to enable it to read the API keys from the .env file
FHIR-Server Integration with fhir-service
The below documentation provides information for integrating the FHIR-Server with the fhir-service using hapi-fhir-jpaserver-starter and enabling authentication through an interceptor.
Integration Steps
Utilize hapi-fhir-jpaserver-starter: Start by integrating the FHIR-Server with the fhir-service using hapi-fhir-jpaserver-starter. This provides a robust foundation for handling FHIR resources and operations.
Enable Authentication: Authentication for the FHIR-Server endpoints are essential for secure communication. This is achieved through an interceptor and API Key Pair.
Implement Authentication Interceptor: Utilize the provided code snippet to implement an authentication interceptor. This interceptor will transmit the access key ID and secret access key to the fhir-service for authentication.
Generate Access Key and Secret Key: In the fhir-service, create an access key ID and secret access key for a FHIR user intending to utilize the fhir-server endpoints. These credentials will serve for authentication when communicating with the FHIR-Server.
packageca.uhn.fhir.jpa.starter.util;importca.uhn.fhir.interceptor.api.Interceptor;importca.uhn.fhir.rest.server.interceptor.InterceptorAdapter;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.http.HttpEntity;importorg.springframework.http.HttpHeaders;importorg.springframework.http.HttpStatus;importorg.springframework.http.MediaType;importorg.springframework.http.ResponseEntity;importorg.springframework.stereotype.Component;importorg.springframework.web.client.HttpClientErrorException;importorg.springframework.web.client.HttpStatusCodeException;importorg.springframework.web.client.RestTemplate;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;importjava.net.InetAddress;importjava.net.UnknownHostException;importjava.util.HashMap;importjava.util.List;importjava.util.Map;importjava.util.Objects;importjava.util.regex.Matcher;importjava.util.regex.Pattern;/** * Interceptor for handling authentication in FHIR requests. * <p> * This class intercepts incoming FHIR requests and performs authentication logic before processing. * It makes a REST call with access key and secret key for authorization. * The class provides custom exceptions for different error scenarios. * */@Component@InterceptorpublicclassAuthenticationInterceptorextendsInterceptorAdapter {privatestaticfinalLogger log =LoggerFactory.getLogger(AuthenticationInterceptor.class); @Value("${app.validate_token_api}")privateString targetUrl; /** * Overrides the incomingRequestPreProcessed method to handle authentication logic. * * @param request The incoming HttpServletRequest. * @param response The HttpServletResponse to be modified if needed. * @return True if the request has been pre-processed; false otherwise. */ @OverridepublicbooleanincomingRequestPreProcessed(HttpServletRequest request,HttpServletResponse response) {String uri =request.getRequestURI();String accessKeyId =request.getHeader(FHIRResources.ACCESS_KEY_ID_PARAM);String secretAccessKey =request.getHeader(FHIRResources.SECRET_ACCESS_KEY_PARAM);Pattern pattern =Pattern.compile(FHIRResources.FHIR_URI_REGEX);Matcher matcher =pattern.matcher(uri);log.info("Incoming request URI {} ",request.getRequestURI());String remoteHost =request.getRemoteHost();String hostAddress =getHostAddress();String remoteHostPrefix =getPrefix(remoteHost,16);String localHostPrefix =getPrefix(hostAddress,16);boolean hostCheck =remoteHostPrefix.equals(localHostPrefix);if (hostCheck) {return super.incomingRequestPreProcessed(request, response); } elseif (matcher.matches() ||isFhirResourceRequest(uri)) {try {makeRestCallWithAuthorization(accessKeyId, secretAccessKey); } catch (IOException e) {log.error("Exception during REST call", e);thrownewUnsupportedOperationException(e); } }return super.incomingRequestPreProcessed(request, response); } /** * Retrieves the host address of the local machine. * * @return The host address. */privateStringgetHostAddress() {try {InetAddress localhost =InetAddress.getLocalHost();returnlocalhost.getHostAddress(); } catch (UnknownHostException e) {log.error("UnknownHostException", e);returnnull; } } /** * Checks if the given URI corresponds to a FHIR resource request. * * @param uri The URI to be checked. * @return True if the URI represents a FHIR resource request; false otherwise. */privatebooleanisFhirResourceRequest(String uri) {List<String> resources =FHIRResources.ALL_RESOURCES; return resources.stream().anyMatch(resource -> uri.contains(FHIRResources.BACK_SLASH + FHIRResources.FHIR + FHIRResources.BACK_SLASH + resource));
} /** * Makes a REST call with the provided access key ID and secret access key for authorization. * * @param accessKeyId The access key ID. * @param secretAccessKey The secret access key. * @throwsIOException If an I/O error occurs. */privatevoidmakeRestCallWithAuthorization(String accessKeyId,String secretAccessKey) throwsIOException {HttpHeaders headers =newHttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);Map<String,String> requestBody =newHashMap<>();requestBody.put(FHIRResources.ACCESS_KEY_ID_PARAM, accessKeyId);requestBody.put(FHIRResources.SECRET_ACCESS_KEY_PARAM, secretAccessKey);if (isValidCredentials(accessKeyId, secretAccessKey)) {HttpEntity<Map<String,String>> requestEntity =newHttpEntity<>(requestBody, headers);RestTemplate restTemplate =newRestTemplate();try { ResponseEntity<String> authResponse = restTemplate.postForEntity(targetUrl, requestEntity, String.class);
if (authResponse.getStatusCode().is2xxSuccessful()) {log.info("authentication service status code: {}",authResponse.getStatusCode()); } else {log.error("Error making REST call. Status code: {}",authResponse.getStatusCodeValue());handleHttpStatusCodeError(authResponse.getStatusCode()); } } catch (HttpClientErrorException.Unauthorized exception) {handleUnauthorizedError(); } catch (HttpStatusCodeException exception) {handleHttpStatusCodeError(exception.getStatusCode()); } catch (Exception exception) {handleGenericError(); } } else {log.error("Invalid accessKeyId or secretAccessKey");thrownewUnauthorizedException("Invalid accessKeyId or secretAccessKey"); } } /** * Checks if the provided access key ID and secret access key are valid. * * @param accessKeyId The access key ID. * @param secretAccessKey The secret access key. * @return True if the credentials are valid; false otherwise. */privatebooleanisValidCredentials(String accessKeyId,String secretAccessKey) { return Objects.nonNull(accessKeyId) && !accessKeyId.isEmpty() && Objects.nonNull(secretAccessKey) && !secretAccessKey.isEmpty();
} /** * Sends an unauthorized response with an error message. */privatevoidhandleUnauthorizedError() {thrownewUnauthorizedException("Invalid accessKeyId or secretAccessKey."); } /** * Handles an HTTP status code error by throwing an exception with the appropriate message. * * @param statusCode The HTTP status code. */privatevoidhandleHttpStatusCodeError(HttpStatus statusCode) {thrownewHttpErrorException("HTTP status code error: "+ statusCode); } /** * Handles a generic error by throwing an internal server error exception. */privatevoidhandleGenericError() {thrownewInternalServerErrorException("An unexpected error occurred."); } /** * Retrieves the prefix of the given IP address with the specified number of bits. * * @param ipAddress The IP address. * @param bits The number of bits for the prefix. * @return The prefix of the IP address. */privateStringgetPrefix(String ipAddress,int bits) {String[] octets =ipAddress.split("\\.");StringBuilder prefix =newStringBuilder();for (int i =0; i <Math.min(4, bits /8); i++) {prefix.append(octets[i]).append("."); }returnprefix.substring(0,prefix.length() -1); } /** * Custom exception for unauthorized errors. */publicstaticclassUnauthorizedExceptionextendsRuntimeException {publicUnauthorizedException(String message) { super(message); } } /** * Custom exception for HTTP status code errors. */publicstaticclassHttpErrorExceptionextendsRuntimeException {publicHttpErrorException(String message) { super(message); } } /** * Custom exception for generic internal server errors. */publicstaticclassInternalServerErrorExceptionextendsRuntimeException {publicInternalServerErrorException(String message) { super(message); } }}
Migration API's
To migrate the user and site use the following API endpoints given below.
Note : These API's use at Once when Server and Service setUp done. No need any RequestBody and Headers for the migration endpoints.
SiteMigration endpoint
GET {{ipAdd}}/fhir-adapter-service/migration/site-migrate
UserMigration endpoint
GET {{ipAdd}}/fhir-adapter-service/migration/user-migrate
Sample FHIR-Mapping Reference
The resources utilized by the FHIR adapter service in conjunction with Spice modules, which encompass Patient, Observation, Practitioner, and Organization Resources. Moreover, the document includes mappings of FHIR attributes associated with these resources. These mappings are crucial for ensuring smooth interoperability between Spice modules and FHIR-based systems, enabling efficient data exchange and translation. Access the FHIR attribute mappings via :
For the mapping convention used in the Spice Patient module, refer to the PatientConverter.java file located at below