Merge remote-tracking branch 'origin/master' into rel_8_5-tracking

This commit is contained in:
dotasek
2025-10-09 09:26:41 -04:00
81 changed files with 3134 additions and 1294 deletions

View File

@@ -5,7 +5,6 @@ target/maven-*
target/ROOT
target/test-classes/
target/war
target/duplicate-finder-result.xml
target/jacoco.exec
target/*.original
.idea

View File

@@ -20,11 +20,12 @@ jobs:
- name: spotless:check
run: mvn spotless:check
- uses: mshick/add-pr-comment@v2
if: always()
# Only run if the previous step failed
if: failure() && steps.spotless_check.outcome == 'failure'
with:
proxy-url: https://slack-bots.azure.smilecdr.com/robogary/github
message-success: |
Formatting check succeeded!
Formatting check succeeded! This should never run, because the step is skipped on workflow success.
message-failure: |
**This Pull Request has failed the formatting check**

38
AGENTS.md Normal file
View File

@@ -0,0 +1,38 @@
# Repository Guidelines
## Project Structure & Module Organization
- `src/main/java`: Spring Boot entry point `ca.uhn.fhir.jpa.starter.Application`, resource providers, config.
- `src/main/resources`: Application YAML, search parameter bundles, capability statements packaged with the WAR.
- `src/main/webapp`: HAPI Testpage overlay shipped for the default UI.
- `src/test/java` & `src/test/resources`: JUnit 5 suites (interceptors, MCP, MDM) plus matching fixtures grouped by FHIR version.
- `charts/`, `docker-compose.yml`, `configs/`: Deployment templates for Helm, Docker, and Tomcat/server overrides.
- `Dockerfile`, `build-docker-image.sh`: Reference container build scripts used by CI/CD.
## Build, Test, and Development Commands
- `mvn clean install`: Compile, run Surefire + Failsafe, and emit `target/ROOT.war`.
- `mvn spring-boot:run -Pboot`: Start the server on port 8080 with hot reload-friendly Boot profile.
- `mvn clean package spring-boot:repackage -Pboot && java -jar target/ROOT.war`: Build and exercise the bootable WAR.
- `docker-compose up -d --build`: Launch JPAServer + PostgreSQL using the local Dockerfile.
- `docker run -p 8080:8080 hapiproject/hapi:latest`: Compare against the upstream binary distribution.
## Coding Style & Naming Conventions
- Target Java 17, four-space indents, alphabetized imports, no wildcards.
- Keep code under `ca.uhn.fhir.jpa.starter` and mirror packages in tests.
- Prefer descriptive class suffixes (`*Provider`, `*Service`, `*Config`) and constructor injection with `final` collaborators.
- YAML keys stay kebab-case; JSON fixtures use lower_snake_case filenames.
## Testing Guidelines
- `mvn test`: Runs JUnit Jupiter unit suites such as `CustomBeanTest` and `ParallelUpdatesVersionConflictTest`.
- `mvn verify`: Adds integration coverage through Failsafe with the default H2 datasource; if you pivot to PostgreSQL, run `mvn install -DskipTests` until fixtures are updated.
- Store integration suites as `*IT.java` so Failsafe detects them and colocate datasets in `src/test/resources`.
- Leverage Testcontainers and HAPI FHIR test utilities already declared in `pom.xml`.
## Commit & Pull Request Guidelines
- Follow repository precedent: imperative summary, optional scope (`Feature/mcp`), and linked issue `(#123)` when applicable.
- Keep commits narrowly scoped and include config or fixture updates with the code they support.
- PRs should describe runtime impact (profiles, ports, env vars), reference issues, and include UI screenshots when behaviour changes.
- Run `mvn verify` or the relevant Docker workflow before review; note any skipped checks and how to reproduce the result.
## Security & Configuration Tips
- Do not commit secrets; stash overrides under `configs/` and explain required env vars in the PR.
- When enabling external services, update `src/main/resources/application.yaml` plus sample overrides and mention connection expectations for reviewers.

View File

@@ -1,7 +1,7 @@
FROM docker.io/library/maven:3.9.9-eclipse-temurin-17 AS build-hapi
WORKDIR /tmp/hapi-fhir-jpaserver-starter
ARG OPENTELEMETRY_JAVA_AGENT_VERSION=1.33.3
ARG OPENTELEMETRY_JAVA_AGENT_VERSION=2.13.1
RUN curl -LSsO https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v${OPENTELEMETRY_JAVA_AGENT_VERSION}/opentelemetry-javaagent.jar
COPY pom.xml .
@@ -18,7 +18,7 @@ RUN mkdir /app && cp /tmp/hapi-fhir-jpaserver-starter/target/ROOT.war /app/main.
########### bitnami tomcat version is suitable for debugging and comes with a shell
########### it can be built using eg. `docker build --target tomcat .`
FROM bitnami/tomcat:10.1 AS tomcat
FROM docker.io/bitnamilegacy/tomcat:10.1.43-debian-12-r0 AS tomcat
USER root
RUN rm -rf /opt/bitnami/tomcat/webapps/ROOT && \

View File

@@ -49,6 +49,21 @@ docker run -p 8080:8080 -e hapi.fhir.default_encoding=xml hapiproject/hapi:lates
HAPI looks in the environment variables for properties in the [application.yaml](https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/application.yaml) file for defaults.
### Binary storage configuration
To stream large `Binary` payloads to disk instead of the database, configure the starter with filesystem storage properties:
```
hapi:
fhir:
binary_storage_enabled: true
binary_storage_mode: FILESYSTEM
binary_storage_filesystem_base_directory: /binstore
# inline_resource_storage_below_size: 131072 # optional override
```
When `binary_storage_mode` is set to `FILESYSTEM` and `inline_resource_storage_below_size` is omitted, the starter automatically applies a 102400 byte (100 KB) inline threshold so smaller payloads remain in the database. Ensure the directory you point to is writable by the process (for Docker builds, mount it into the container with appropriate permissions).
### Configuration via overridden application.yaml file and using Docker
You can customize HAPI by telling HAPI to look for the configuration file in a different location, e.g.:
@@ -65,6 +80,12 @@ docker run -p 8090:8080 -e "--spring.config.location=classpath:/another.applicat
```
Here, the configuration file (*another.application.yaml*) is part of the compiled set of resources.
### One-liner for quickly getting an Implementation Guide installed into HAPI
```
docker run -p 8080:8080 -e "hapi.fhir.implementationguides.someIg.name=com.org.something" -e "hapi.fhir.implementationguides.someIg.version=1.2.3" -e "hapi.fhir.implementationguides.someIg.packageUrl=https://build.fhir.org/ig/yourOrg/yourIg/package.tgz" -e "hapi.fhir.implementationguides.someIg.installMode=STORE_AND_INSTALL" hapiproject/hapi:latest
```
### Example using ``docker-compose.yml`` for docker-compose
```yaml
@@ -489,7 +510,7 @@ The server may be configured with subscription support by enabling properties in
## Enabling Clinical Reasoning
Set `hapi.fhir.cr.enabled=true` in the [application.yaml](https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/application.yaml) file to enable [Clinical Quality Language](https://cql.hl7.org/) on this server. An alternate settings file, [cds.application.yaml](https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/cds.application.yaml), exists with the Clinical Reasoning module enabled and default settings that have been found to work with most CDS and dQM test cases.
Set `hapi.fhir.cr.enabled=true` in the [application.yaml](https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/application.yaml) file to enable [Clinical Quality Language](https://cql.hl7.org/) on this server. An alternate settings file, [application-cds.yaml](https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/application-cds.yaml), exists with the Clinical Reasoning module enabled and default settings that have been found to work with most CDS and dQM test cases.
## Enabling CDS Hooks
@@ -588,3 +609,7 @@ docker run --rm -it -p 8080:8080 \
```
You can configure the agent using environment variables or Java system properties, see <https://opentelemetry.io/docs/instrumentation/java/automatic/agent-config/> for details.
## Enable MCP
MCP capabilities can be enabled by setting the `spring.ai.mcp.server.enabled` to `true`. This will enable the MCP server and expose the MCP endpoints. The MCP endpoint is currently hardcoded to `/mcp/message` and can be tried out by running e.g. `npx @modelcontextprotocol/inspector` and connect to http://localhost:8080/mcp/message using Streamable HTTP. Spring AI MCP Server Auto Configuration is currently not supported.

View File

@@ -1,9 +1,9 @@
dependencies:
- name: postgresql
repository: oci://registry-1.docker.io/bitnamicharts
version: 16.7.11
version: 16.7.27
- name: common
repository: oci://registry-1.docker.io/bitnamicharts
version: 2.31.3
digest: sha256:e8b5591d28c8b420a68c1bef3ac8530f47c0c9c5d22fddec3c73f45ae5ba615a
generated: "2025-06-16T13:22:01.211160104+02:00"
digest: sha256:4ac06ee8266b694791c14eaef2e0f19a5714ebe4bd19b1bcb4bc6069d8fab482
generated: "2025-09-22T11:26:51.06616649+02:00"

View File

@@ -7,14 +7,14 @@ sources:
- https://github.com/hapifhir/hapi-fhir-jpaserver-starter
dependencies:
- name: postgresql
version: 16.7.11
version: 16.7.27
repository: oci://registry-1.docker.io/bitnamicharts
condition: postgresql.enabled
- name: common
repository: oci://registry-1.docker.io/bitnamicharts
version: 2.31.3
appVersion: 8.2.0
version: 0.20.1
version: 0.21.0
annotations:
artifacthub.io/license: Apache-2.0
artifacthub.io/containsSecurityUpdates: "false"
@@ -27,4 +27,4 @@ annotations:
# When using the list of objects option the valid supported kinds are
# added, changed, deprecated, removed, fixed, and security.
- kind: changed
description: "fixed typo in README.md"
description: "Use the bitnamilegacy repos for the images. See <https://github.com/bitnami/containers/issues/83267>."

View File

@@ -1,6 +1,6 @@
# HAPI FHIR JPA Server Starter Helm Chart
![Version: 0.20.1](https://img.shields.io/badge/Version-0.20.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 8.2.0](https://img.shields.io/badge/AppVersion-8.2.0-informational?style=flat-square)
![Version: 0.21.0](https://img.shields.io/badge/Version-0.21.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 8.2.0](https://img.shields.io/badge/AppVersion-8.2.0-informational?style=flat-square)
This helm chart will help you install the HAPI FHIR JPA Server in a Kubernetes environment.
@@ -16,7 +16,7 @@ helm install hapi-fhir-jpaserver hapifhir/hapi-fhir-jpaserver
| Repository | Name | Version |
|------------|------|---------|
| oci://registry-1.docker.io/bitnamicharts | common | 2.31.3 |
| oci://registry-1.docker.io/bitnamicharts | postgresql | 16.7.11 |
| oci://registry-1.docker.io/bitnamicharts | postgresql | 16.7.27 |
## Values
@@ -62,6 +62,7 @@ helm install hapi-fhir-jpaserver hapifhir/hapi-fhir-jpaserver
| postgresql.auth.database | string | `"fhir"` | name for a custom database to create |
| postgresql.auth.existingSecret | string | `""` | Name of existing secret to use for PostgreSQL credentials `auth.postgresPassword`, `auth.password`, and `auth.replicationPassword` will be ignored and picked up from this secret The secret must contain the keys `postgres-password` (which is the password for "postgres" admin user), `password` (which is the password for the custom user to create when `auth.username` is set), and `replication-password` (which is the password for replication user). The secret might also contains the key `ldap-password` if LDAP is enabled. `ldap.bind_password` will be ignored and picked from this secret in this case. The value is evaluated as a template. |
| postgresql.enabled | bool | `true` | enable an included PostgreSQL DB. see <https://github.com/bitnami/charts/tree/master/bitnami/postgresql> for details if set to `false`, the values under `externalDatabase` are used |
| postgresql.image.repository | string | `"bitnamilegacy/postgresql"` | |
| replicaCount | int | `1` | number of replicas to deploy |
| resources | object | `{}` | configure the FHIR server's resource requests and limits |
| resourcesPreset | string | `"medium"` | set container resources according to one common preset (allowed values: none, nano, micro, small, medium, large, xlarge, 2xlarge). This is ignored if `resources` is set (`resources` is recommended for production). More information: <https://github.com/bitnami/charts/blob/main/bitnami/common/templates/_resources.tpl#L15> |
@@ -84,7 +85,7 @@ helm install hapi-fhir-jpaserver hapifhir/hapi-fhir-jpaserver
| tests.resourcesPreset | string | `"nano"` | set container resources according to one common preset (allowed values: none, nano, micro, small, medium, large, xlarge, 2xlarge). This is ignored if `resources` is set (`resources` is recommended for production). More information: <https://github.com/bitnami/charts/blob/main/bitnami/common/templates/_resources.tpl#L15> |
| tolerations | list | `[]` | pod tolerations |
| topologySpreadConstraints | list | `[]` | pod topology spread configuration see: https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/#api |
| waitForDatabaseInitContainer.image | object | `{"pullPolicy":"IfNotPresent","registry":"docker.io","repository":"bitnami/postgresql","tag":"17.5.0-debian-12-r11@sha256:ac8dd0d6512c4c5fb146c16b1c5f05862bd5f600d73348506ab4252587e7fcc6"}` | image to use for the init container which waits until the database is ready to accept connections |
| waitForDatabaseInitContainer.image | object | `{"pullPolicy":"IfNotPresent","registry":"docker.io","repository":"bitnamilegacy/postgresql","tag":"17.6.0-debian-12-r4@sha256:926356130b77d5742d8ce605b258d35db9b62f2f8fd1601f9dbaef0c8a710a8d"}` | image to use for the init container which waits until the database is ready to accept connections |
## Development

View File

@@ -114,11 +114,19 @@ topologySpreadConstraints:
# app.kubernetes.io/instance: hapi-fhir-jpaserver
# app.kubernetes.io/name: hapi-fhir-jpaserver
# @ignored
# used by the bitnami sub-chart. Allowing it allows for overrding the image repository.
global:
security:
allowInsecureImages: true
postgresql:
# -- enable an included PostgreSQL DB.
# see <https://github.com/bitnami/charts/tree/master/bitnami/postgresql> for details
# if set to `false`, the values under `externalDatabase` are used
enabled: true
image:
repository: bitnamilegacy/postgresql
auth:
# -- name for a custom database to create
database: "fhir"
@@ -306,6 +314,6 @@ waitForDatabaseInitContainer:
# is ready to accept connections
image:
registry: docker.io
repository: bitnami/postgresql
tag: 17.5.0-debian-12-r11@sha256:ac8dd0d6512c4c5fb146c16b1c5f05862bd5f600d73348506ab4252587e7fcc6
repository: bitnamilegacy/postgresql
tag: 17.6.0-debian-12-r4@sha256:926356130b77d5742d8ce605b258d35db9b62f2f8fd1601f9dbaef0c8a710a8d
pullPolicy: IfNotPresent

39
pom.xml
View File

@@ -5,8 +5,8 @@
<properties>
<java.version>17</java.version>
<hapi.fhir.jpa.server.starter.revision>1</hapi.fhir.jpa.server.starter.revision>
<clinical-reasoning.version>3.24.0</clinical-reasoning.version>
<hapi.fhir.jpa.server.starter.revision>3</hapi.fhir.jpa.server.starter.revision>
<clinical-reasoning.version>3.27.0</clinical-reasoning.version>
</properties>
<!-- one-liner to take you to the cloud with settings form the application.yaml file: -->
@@ -60,6 +60,16 @@
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<version>${spring_boot_version}</version>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
@@ -265,12 +275,7 @@
<artifactId>moment</artifactId>
</dependency>
<!-- The following dependencies are only needed for automated unit tests, you do not neccesarily need them to run the example. -->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
@@ -383,6 +388,24 @@
<version>5.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp</artifactId>
<version>1.1.0-M2</version>
</dependency>
<!--implementation("org.springframework.ai:spring-ai-starter-mcp-server-webmvc:1.1.0-M1")-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
<version>1.1.0-M2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>

View File

@@ -0,0 +1,108 @@
/*
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2025 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoObservation;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.RawParam;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.DateAndListParam;
import ca.uhn.fhir.rest.param.ReferenceAndListParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.List;
import java.util.Map;
// Can be removed when https://github.com/hapifhir/hapi-fhir/issues/7255 is resolved
public abstract class BaseJpaResourceProviderObservation<T extends IBaseResource> extends BaseJpaResourceProvider<T> {
/**
* Observation/$lastn
*/
@Operation(name = JpaConstants.OPERATION_LASTN, idempotent = true, bundleType = BundleTypeEnum.SEARCHSET)
public IBundleProvider observationLastN(
jakarta.servlet.http.HttpServletRequest theServletRequest,
jakarta.servlet.http.HttpServletResponse theServletResponse,
ca.uhn.fhir.rest.api.server.RequestDetails theRequestDetails,
@Description(
formalDefinition =
"Results from this method are returned across multiple pages. This parameter controls the size of those pages.")
@OperationParam(name = Constants.PARAM_COUNT, typeName = "unsignedInt")
IPrimitiveType<Integer> theCount,
@Description(shortDefinition = "The classification of the type of observation")
@OperationParam(name = "category")
TokenAndListParam theCategory,
@Description(shortDefinition = "The code of the observation type") @OperationParam(name = "code")
TokenAndListParam theCode,
@Description(shortDefinition = "The effective date of the observation") @OperationParam(name = "date")
DateAndListParam theDate,
@Description(shortDefinition = "The subject that the observation is about (if patient)")
@OperationParam(name = "patient")
ReferenceAndListParam thePatient,
@Description(shortDefinition = "The subject that the observation is about")
@OperationParam(name = "subject")
ReferenceAndListParam theSubject,
@Description(shortDefinition = "The maximum number of observations to return for each observation code")
@OperationParam(name = "max", typeName = "integer", min = 0, max = 1)
IPrimitiveType<Integer> theMax,
@RawParam Map<String, List<String>> theAdditionalRawParams) {
startRequest(theServletRequest);
try {
SearchParameterMap paramMap = new SearchParameterMap();
paramMap.add(org.hl7.fhir.r4.model.Observation.SP_CATEGORY, theCategory);
paramMap.add(org.hl7.fhir.r4.model.Observation.SP_CODE, theCode);
paramMap.add(org.hl7.fhir.r4.model.Observation.SP_DATE, theDate);
if (thePatient != null) {
paramMap.add(org.hl7.fhir.r4.model.Observation.SP_PATIENT, thePatient);
}
if (theSubject != null) {
paramMap.add(org.hl7.fhir.r4.model.Observation.SP_SUBJECT, theSubject);
}
if (theMax != null) {
paramMap.setLastNMax(theMax.getValue());
/**
* The removal of the original raw parameter is required as every implementing class
* has the "Observation" resource class defined. For this resource, the max parameter
* is not supported and thus has to be removed before the use of "translateRawParameters".
*/
if (theAdditionalRawParams != null) theAdditionalRawParams.remove("max");
}
if (theCount != null) {
paramMap.setCount(theCount.getValue());
}
getDao().translateRawParameters(theAdditionalRawParams, paramMap);
return ((IFhirResourceDaoObservation<?>) getDao())
.observationsLastN(paramMap, theRequestDetails, theServletResponse);
} finally {
endRequest(theServletRequest);
}
}
}

View File

@@ -5,7 +5,7 @@ import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings.ClientIdStrategyEnum;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings.IdStrategyEnum;
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
import ca.uhn.fhir.jpa.packages.PackageInstallationSpec;
import ca.uhn.fhir.jpa.starter.ig.ExtendedPackageInstallationSpec;
import ca.uhn.fhir.rest.api.EncodingEnum;
import org.hl7.fhir.r4.model.Bundle;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -21,9 +21,9 @@ import java.util.Set;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "hapi.fhir")
@Configuration
@EnableConfigurationProperties
public class AppProperties {
private final Set<String> auto_version_reference_at_paths = new HashSet<>();
@@ -61,7 +61,15 @@ public class AppProperties {
private Boolean filter_search_enabled = true;
private Boolean graphql_enabled = false;
private Boolean binary_storage_enabled = false;
private Integer inline_resource_storage_below_size = 0;
public enum BinaryStorageMode {
DATABASE,
FILESYSTEM
}
private BinaryStorageMode binary_storage_mode = BinaryStorageMode.DATABASE;
private String binary_storage_filesystem_base_directory;
private Integer inline_resource_storage_below_size;
private Boolean bulk_export_enabled = false;
private Boolean bulk_import_enabled = false;
private Boolean default_pretty_print = true;
@@ -88,7 +96,9 @@ public class AppProperties {
private Partitioning partitioning = null;
private Boolean validate_resource_status_for_package_upload = true;
private Boolean install_transitive_ig_dependencies = true;
private Map<String, PackageInstallationSpec> implementationGuides = null;
private List<String> install_additional_resources_from_ig_folders = new ArrayList<>();
private Map<String, ExtendedPackageInstallationSpec> implementationGuides = null;
private String custom_content_path = null;
private String app_content_path = null;
private Boolean lastn_enabled = false;
@@ -116,6 +126,8 @@ public class AppProperties {
private Map<String, RemoteSystem> remote_terminology_service = null;
private Boolean match_url_cache_enabled = false;
private Boolean index_storage_optimized = false;
private Integer reindex_thread_count = null;
private Integer expunge_thread_count = null;
public List<String> getCustomInterceptorClasses() {
return custom_interceptor_classes;
@@ -157,11 +169,11 @@ public class AppProperties {
this.defer_indexing_for_codesystems_of_size = defer_indexing_for_codesystems_of_size;
}
public Map<String, PackageInstallationSpec> getImplementationGuides() {
public Map<String, ExtendedPackageInstallationSpec> getImplementationGuides() {
return implementationGuides;
}
public void setImplementationGuides(Map<String, PackageInstallationSpec> implementationGuides) {
public void setImplementationGuides(Map<String, ExtendedPackageInstallationSpec> implementationGuides) {
this.implementationGuides = implementationGuides;
}
@@ -481,6 +493,22 @@ public class AppProperties {
this.binary_storage_enabled = binary_storage_enabled;
}
public BinaryStorageMode getBinary_storage_mode() {
return binary_storage_mode;
}
public void setBinary_storage_mode(BinaryStorageMode binary_storage_mode) {
this.binary_storage_mode = binary_storage_mode;
}
public String getBinary_storage_filesystem_base_directory() {
return binary_storage_filesystem_base_directory;
}
public void setBinary_storage_filesystem_base_directory(String binary_storage_filesystem_base_directory) {
this.binary_storage_filesystem_base_directory = binary_storage_filesystem_base_directory;
}
public Integer getInline_resource_storage_below_size() {
return inline_resource_storage_below_size;
}
@@ -710,6 +738,15 @@ public class AppProperties {
this.resource_dbhistory_enabled = resource_dbhistory_enabled;
}
public List<String> getInstall_additional_resources_from_ig_folders() {
return install_additional_resources_from_ig_folders;
}
public void setInstall_additional_resources_from_ig_folders(
List<String> install_additional_resources_from_ig_folders) {
this.install_additional_resources_from_ig_folders = install_additional_resources_from_ig_folders;
}
public Boolean getPre_expand_value_sets() {
return this.pre_expand_value_sets;
}
@@ -774,6 +811,22 @@ public class AppProperties {
index_storage_optimized = theIndex_storage_optimized;
}
public Integer getReindex_thread_count() {
return reindex_thread_count;
}
public void setReindex_thread_count(Integer reindex_thread_count) {
this.reindex_thread_count = reindex_thread_count;
}
public Integer getExpunge_thread_count() {
return expunge_thread_count;
}
public void setExpunge_thread_count(Integer expunge_thread_count) {
this.expunge_thread_count = expunge_thread_count;
}
public JpaStorageSettings.StoreMetaSourceInformationEnum getStore_meta_source_information() {
return store_meta_source_information;
}

View File

@@ -16,7 +16,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
@@ -26,7 +25,7 @@ import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Import;
@ServletComponentScan(basePackageClasses = {RestfulServer.class})
@SpringBootApplication(exclude = {ElasticsearchRestClientAutoConfiguration.class, ThymeleafAutoConfiguration.class})
@SpringBootApplication(exclude = {ThymeleafAutoConfiguration.class})
@Import({
StarterCrR4Config.class,
StarterCrDstu3Config.class,

View File

@@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.starter.annotations;
import ca.uhn.fhir.jpa.starter.AppProperties;
import org.springframework.boot.context.properties.bind.Binder;
import ca.uhn.fhir.jpa.starter.util.EnvironmentHelper;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
@@ -10,9 +10,7 @@ public class OnImplementationGuidesPresent implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) {
AppProperties config = Binder.get(conditionContext.getEnvironment())
.bind("hapi.fhir", AppProperties.class)
.orElse(null);
AppProperties config = EnvironmentHelper.getConfiguration(conditionContext, "hapi.fhir", AppProperties.class);
if (config == null) return false;
if (config.getImplementationGuides() == null) return false;
return !config.getImplementationGuides().isEmpty();

View File

@@ -1,7 +1,9 @@
package ca.uhn.fhir.jpa.starter.cdshooks;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "hapi.fhir.cdshooks")
public class CdsHooksProperties {

View File

@@ -2,7 +2,6 @@ package ca.uhn.fhir.jpa.starter.cdshooks;
import ca.uhn.fhir.jpa.starter.AppProperties;
import ca.uhn.fhir.rest.api.server.cdshooks.CdsServiceRequestJson;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceRegistry;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
@@ -43,9 +42,6 @@ public class CdsHooksServlet extends HttpServlet {
@Autowired
ICdsServiceRegistry cdsServiceRegistry;
@Autowired
RestfulServer restfulServer;
@Autowired
@Qualifier(CDS_HOOKS_OBJECT_MAPPER_FACTORY)
ObjectMapper objectMapper;

View File

@@ -1,6 +1,6 @@
package ca.uhn.fhir.jpa.starter.cdshooks;
import ca.uhn.fhir.jpa.starter.cr.CrProperties;
import ca.uhn.fhir.jpa.starter.cr.CqlRuntimeProperties;
public class ProviderConfiguration {
private final String clientIdHeaderName;
@@ -11,8 +11,8 @@ public class ProviderConfiguration {
this.clientIdHeaderName = clientIdHeaderName;
}
public ProviderConfiguration(CdsHooksProperties cdsProperties, CrProperties crProperties) {
this(crProperties.getCql().getRuntime().isDebugLoggingEnabled(), cdsProperties.getClientIdHeaderName());
public ProviderConfiguration(CdsHooksProperties cdsProperties, CqlRuntimeProperties cqlRuntimeProperties) {
this(cqlRuntimeProperties.isDebugLoggingEnabled(), cdsProperties.getClientIdHeaderName());
}
public String getClientIdHeaderName() {

View File

@@ -1,20 +1,13 @@
package ca.uhn.fhir.jpa.starter.cdshooks;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.jpa.starter.cr.CrCommonConfig;
import ca.uhn.fhir.jpa.starter.cr.CrConfigCondition;
import ca.uhn.fhir.jpa.starter.cr.CrProperties;
import ca.uhn.fhir.jpa.starter.cr.*;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsHooksDaoAuthorizationSvc;
import ca.uhn.hapi.fhir.cdshooks.config.CdsHooksConfig;
import ca.uhn.hapi.fhir.cdshooks.svc.CdsHooksContextBooter;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.opencds.cqf.fhir.cr.hapi.cdshooks.CdsCrServiceRegistry;
import org.opencds.cqf.fhir.cr.hapi.cdshooks.CdsCrSettings;
import org.opencds.cqf.fhir.cr.hapi.cdshooks.ICdsCrServiceRegistry;
import org.opencds.cqf.fhir.cr.hapi.cdshooks.discovery.CdsCrDiscoveryServiceRegistry;
import org.opencds.cqf.fhir.cr.hapi.cdshooks.discovery.ICdsCrDiscoveryServiceRegistry;
import org.opencds.cqf.fhir.cr.hapi.config.CrCdsHooksConfig;
import org.opencds.cqf.fhir.cr.hapi.config.RepositoryConfig;
import org.opencds.cqf.fhir.cr.hapi.config.test.TestCdsHooksConfig;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
@@ -24,30 +17,9 @@ import org.springframework.context.annotation.Import;
@Configuration
@Conditional({CdsHooksConfigCondition.class, CrConfigCondition.class})
@Import({RepositoryConfig.class, TestCdsHooksConfig.class, CrCdsHooksConfig.class, CrCommonConfig.class})
@Import({RepositoryConfig.class, CrCdsHooksConfig.class, CrCommonConfig.class, CdsHooksConfig.class})
public class StarterCdsHooksConfig {
@Bean
public ICdsCrDiscoveryServiceRegistry cdsCrDiscoveryServiceRegistry() {
CdsCrDiscoveryServiceRegistry registry = new CdsCrDiscoveryServiceRegistry();
registry.unregister(FhirVersionEnum.R4);
registry.register(FhirVersionEnum.R4, UpdatedCrDiscoveryService.class);
return registry;
}
@Bean
public ICdsCrServiceRegistry cdsCrServiceRegistry() {
CdsCrServiceRegistry registry = new CdsCrServiceRegistry();
registry.unregister(FhirVersionEnum.R4);
registry.register(FhirVersionEnum.R4, UpdatedCdsCrService.class);
return registry;
}
@Bean
public CdsHooksProperties cdsHooksProperties() {
return new CdsHooksProperties();
}
@Bean
public CdsCrSettings cdsCrSettings(CdsHooksProperties cdsHooksProperties) {
CdsCrSettings settings = CdsCrSettings.getDefault();
@@ -67,8 +39,9 @@ public class StarterCdsHooksConfig {
}
@Bean
public ProviderConfiguration providerConfiguration(CdsHooksProperties cdsProperties, CrProperties crProperties) {
return new ProviderConfiguration(cdsProperties, crProperties);
public ProviderConfiguration providerConfiguration(
CdsHooksProperties cdsProperties, CqlProperties cqlProperties, CqlRuntimeProperties cqlRuntimeProperties) {
return new ProviderConfiguration(cdsProperties, cqlRuntimeProperties);
}
@Bean

View File

@@ -1,42 +0,0 @@
package ca.uhn.fhir.jpa.starter.cdshooks;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.repository.IRepository;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.cdshooks.CdsServiceRequestJson;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.opencds.cqf.fhir.cr.hapi.cdshooks.CdsCrService;
import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory;
import static org.opencds.cqf.fhir.utility.Constants.APPLY_PARAMETER_DATA;
public class UpdatedCdsCrService extends CdsCrService {
private final IAdapterFactory adapterFactory;
public UpdatedCdsCrService(RequestDetails theRequestDetails, IRepository theRepository) {
super(theRequestDetails, theRepository);
adapterFactory = IAdapterFactory.forFhirContext(theRepository.fhirContext());
}
@Override
public IBaseParameters encodeParams(CdsServiceRequestJson theJson) {
var parameters = adapterFactory.createParameters(super.encodeParams(theJson));
if (parameters.hasParameter(APPLY_PARAMETER_DATA)) {
parameters.addParameter(
"useServerData",
booleanTypeForVersion(parameters.fhirContext().getVersion().getVersion(), false));
}
return (IBaseParameters) parameters.get();
}
private IPrimitiveType<Boolean> booleanTypeForVersion(FhirVersionEnum fhirVersion, boolean value) {
return switch (fhirVersion) {
case DSTU2 -> new org.hl7.fhir.dstu2.model.BooleanType(value);
case DSTU3 -> new org.hl7.fhir.dstu3.model.BooleanType(value);
case R4 -> new org.hl7.fhir.r4.model.BooleanType(value);
case R5 -> new org.hl7.fhir.r5.model.BooleanType(value);
default -> throw new IllegalArgumentException("unknown or unsupported FHIR version");
};
}
}

View File

@@ -1,12 +0,0 @@
package ca.uhn.fhir.jpa.starter.cdshooks;
import ca.uhn.fhir.repository.IRepository;
import org.hl7.fhir.instance.model.api.IIdType;
import org.opencds.cqf.fhir.cr.hapi.cdshooks.discovery.CrDiscoveryService;
public class UpdatedCrDiscoveryService extends CrDiscoveryService {
public UpdatedCrDiscoveryService(IIdType thePlanDefinitionId, IRepository theRepository) {
super(thePlanDefinitionId, theRepository);
maxUriLength = 6000;
}
}

View File

@@ -1,31 +0,0 @@
package ca.uhn.fhir.jpa.starter.common;
import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl;
import ca.uhn.fhir.jpa.starter.util.EnvironmentHelper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
/** Shared configuration for Elasticsearch */
@Configuration
public class ElasticsearchConfig {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ElasticsearchConfig.class);
@Bean
public ElasticsearchSvcImpl elasticsearchSvc(ConfigurableEnvironment configurableEnvironment) {
if (EnvironmentHelper.isElasticsearchEnabled(configurableEnvironment)) {
String elasticsearchUrl = EnvironmentHelper.getElasticsearchServerUrl(configurableEnvironment);
if (elasticsearchUrl.startsWith("http")) {
elasticsearchUrl = elasticsearchUrl.substring(elasticsearchUrl.indexOf("://") + 3);
}
String elasticsearchProtocol = EnvironmentHelper.getElasticsearchServerProtocol(configurableEnvironment);
String elasticsearchUsername = EnvironmentHelper.getElasticsearchServerUsername(configurableEnvironment);
String elasticsearchPassword = EnvironmentHelper.getElasticsearchServerPassword(configurableEnvironment);
ourLog.info("Configuring elasticsearch {} {}", elasticsearchProtocol, elasticsearchUrl);
return new ElasticsearchSvcImpl(
elasticsearchProtocol, elasticsearchUrl, elasticsearchUsername, elasticsearchPassword);
} else {
return null;
}
}
}

View File

@@ -1,14 +1,15 @@
package ca.uhn.fhir.jpa.starter.common;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.binary.api.IBinaryStorageSvc;
import ca.uhn.fhir.jpa.binstore.DatabaseBinaryContentStorageSvcImpl;
import ca.uhn.fhir.jpa.binstore.FilesystemBinaryStorageSvcImpl;
import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.config.PartitionSettings.CrossPartitionReferenceMode;
import ca.uhn.fhir.jpa.model.config.SubscriptionSettings;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.starter.AppProperties;
import ca.uhn.fhir.jpa.starter.elastic.ElasticsearchBootSvcImpl;
import ca.uhn.fhir.jpa.starter.util.JpaHibernatePropertiesProvider;
import ca.uhn.fhir.jpa.subscription.match.deliver.email.EmailSenderImpl;
import ca.uhn.fhir.jpa.subscription.match.deliver.email.IEmailSender;
@@ -18,13 +19,12 @@ import com.google.common.base.Strings;
import org.hl7.fhir.r4.model.Bundle.BundleType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.*;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.util.Assert;
import java.util.HashSet;
import java.util.stream.Collectors;
@@ -36,9 +36,11 @@ import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
*/
@Configuration
@EnableTransactionManagement
@Import(ElasticsearchBootSvcImpl.class)
public class FhirServerConfigCommon {
private static final Logger ourLog = LoggerFactory.getLogger(FhirServerConfigCommon.class);
private static final int DEFAULT_FILESYSTEM_INLINE_THRESHOLD = 102_400;
public FhirServerConfigCommon(AppProperties appProperties) {
ourLog.info(
@@ -223,8 +225,9 @@ public class FhirServerConfigCommon {
jpaStorageSettings.setLastNEnabled(true);
}
if (appProperties.getInline_resource_storage_below_size() != 0) {
jpaStorageSettings.setInlineResourceTextBelowSize(appProperties.getInline_resource_storage_below_size());
Integer inlineResourceThreshold = resolveInlineResourceThreshold(appProperties);
if (inlineResourceThreshold != null && inlineResourceThreshold != 0) {
jpaStorageSettings.setInlineResourceTextBelowSize(inlineResourceThreshold);
}
jpaStorageSettings.setStoreResourceInHSearchIndex(appProperties.getStore_resource_in_lucene_index_enabled());
@@ -274,7 +277,30 @@ public class FhirServerConfigCommon {
ourLog.debug("Server configured to Store Meta Source: {}", appProperties.getStore_meta_source_information());
jpaStorageSettings.setStoreMetaSourceInformation(appProperties.getStore_meta_source_information());
storageSettings(appProperties, jpaStorageSettings);
jpaStorageSettings.setAllowContainsSearches(appProperties.getAllow_contains_searches());
jpaStorageSettings.setAllowExternalReferences(appProperties.getAllow_external_references());
jpaStorageSettings.setDefaultSearchParamsCanBeOverridden(
appProperties.getAllow_override_default_search_params());
jpaStorageSettings.setNormalizedQuantitySearchLevel(appProperties.getNormalized_quantity_search_level());
jpaStorageSettings.setIndexOnContainedResources(appProperties.getEnable_index_contained_resource());
jpaStorageSettings.setIndexIdentifierOfType(appProperties.getEnable_index_of_type());
// Configure thread counts for reindex and expunge operations
if (appProperties.getReindex_thread_count() != null) {
jpaStorageSettings.setReindexThreadCount(appProperties.getReindex_thread_count());
ourLog.info(
"Server configured to use {} threads for reindex operations",
appProperties.getReindex_thread_count());
}
if (appProperties.getExpunge_thread_count() != null) {
jpaStorageSettings.setExpungeThreadCount(appProperties.getExpunge_thread_count());
ourLog.info(
"Server configured to use {} threads for expunge operations",
appProperties.getExpunge_thread_count());
}
return jpaStorageSettings;
}
@@ -325,11 +351,6 @@ public class FhirServerConfigCommon {
return retVal;
}
@Bean
public PartitionModeConfigurer partitionModeConfigurer() {
return new PartitionModeConfigurer();
}
@Primary
@Bean
public HibernatePropertiesProvider jpaStarterDialectProvider(
@@ -337,29 +358,50 @@ public class FhirServerConfigCommon {
return new JpaHibernatePropertiesProvider(myEntityManagerFactory);
}
protected StorageSettings storageSettings(AppProperties appProperties, JpaStorageSettings jpaStorageSettings) {
jpaStorageSettings.setAllowContainsSearches(appProperties.getAllow_contains_searches());
jpaStorageSettings.setAllowExternalReferences(appProperties.getAllow_external_references());
jpaStorageSettings.setDefaultSearchParamsCanBeOverridden(
appProperties.getAllow_override_default_search_params());
jpaStorageSettings.setNormalizedQuantitySearchLevel(appProperties.getNormalized_quantity_search_level());
jpaStorageSettings.setIndexOnContainedResources(appProperties.getEnable_index_contained_resource());
jpaStorageSettings.setIndexIdentifierOfType(appProperties.getEnable_index_of_type());
return jpaStorageSettings;
}
@Lazy
@Bean
public IBinaryStorageSvc binaryStorageSvc(AppProperties appProperties) {
DatabaseBinaryContentStorageSvcImpl binaryStorageSvc = new DatabaseBinaryContentStorageSvcImpl();
@ConditionalOnProperty(prefix = "hapi.fhir", name = "binary_storage_mode", havingValue = "FILESYSTEM")
public FilesystemBinaryStorageSvcImpl filesystemBinaryStorageSvc(AppProperties appProperties) {
String baseDirectory = appProperties.getBinary_storage_filesystem_base_directory();
Assert.hasText(
baseDirectory,
"binary_storage_filesystem_base_directory must be provided when binary_storage_mode=FILESYSTEM");
if (appProperties.getMax_binary_size() != null) {
binaryStorageSvc.setMaximumBinarySize(appProperties.getMax_binary_size());
FilesystemBinaryStorageSvcImpl filesystemSvc = new FilesystemBinaryStorageSvcImpl(baseDirectory);
Integer inlineResourceThreshold = resolveInlineResourceThreshold(appProperties);
int minimumBinarySize =
inlineResourceThreshold == null ? DEFAULT_FILESYSTEM_INLINE_THRESHOLD : inlineResourceThreshold;
filesystemSvc.setMinimumBinarySize(minimumBinarySize);
Integer maxBinarySize = appProperties.getMax_binary_size();
if (maxBinarySize != null) {
filesystemSvc.setMaximumBinarySize(maxBinarySize.longValue());
}
return binaryStorageSvc;
return filesystemSvc;
}
@Bean
@ConditionalOnProperty(
prefix = "hapi.fhir",
name = "binary_storage_mode",
havingValue = "DATABASE",
matchIfMissing = true)
public DatabaseBinaryContentStorageSvcImpl databaseBinaryStorageSvc(AppProperties appProperties) {
DatabaseBinaryContentStorageSvcImpl databaseSvc = new DatabaseBinaryContentStorageSvcImpl();
Integer maxBinarySize = appProperties.getMax_binary_size();
if (maxBinarySize != null) {
databaseSvc.setMaximumBinarySize(maxBinarySize.longValue());
}
return databaseSvc;
}
private Integer resolveInlineResourceThreshold(AppProperties appProperties) {
Integer inlineResourceThreshold = appProperties.getInline_resource_storage_below_size();
if (inlineResourceThreshold == null
&& appProperties.getBinary_storage_mode() == AppProperties.BinaryStorageMode.FILESYSTEM) {
return DEFAULT_FILESYSTEM_INLINE_THRESHOLD;
}
return inlineResourceThreshold;
}
@Bean

View File

@@ -9,5 +9,5 @@ import org.springframework.context.annotation.Import;
@Configuration
@Conditional(OnDSTU3Condition.class)
@Import({JpaDstu3Config.class, StarterJpaConfig.class, StarterCrDstu3Config.class, ElasticsearchConfig.class})
@Import({JpaDstu3Config.class, StarterJpaConfig.class, StarterCrDstu3Config.class})
public class FhirServerConfigDstu3 {}

View File

@@ -10,11 +10,5 @@ import org.springframework.context.annotation.Import;
@Configuration
@Conditional(OnR4Condition.class)
@Import({
JpaR4Config.class,
StarterJpaConfig.class,
StarterCrR4Config.class,
ElasticsearchConfig.class,
StarterIpsConfig.class
})
@Import({JpaR4Config.class, StarterJpaConfig.class, StarterCrR4Config.class, StarterIpsConfig.class})
public class FhirServerConfigR4 {}

View File

@@ -9,5 +9,5 @@ import org.springframework.context.annotation.Import;
@Configuration
@Conditional(OnR4BCondition.class)
@Import({JpaR4BConfig.class, SubscriptionTopicConfig.class, StarterJpaConfig.class, ElasticsearchConfig.class})
@Import({JpaR4BConfig.class, SubscriptionTopicConfig.class, StarterJpaConfig.class})
public class FhirServerConfigR4B {}

View File

@@ -9,5 +9,5 @@ import org.springframework.context.annotation.Import;
@Configuration
@Conditional(OnR5Condition.class)
@Import({StarterJpaConfig.class, JpaR5Config.class, SubscriptionTopicConfig.class, ElasticsearchConfig.class})
@Import({StarterJpaConfig.class, JpaR5Config.class, SubscriptionTopicConfig.class})
public class FhirServerConfigR5 {}

View File

@@ -0,0 +1,16 @@
package ca.uhn.fhir.jpa.starter.common;
import ca.uhn.fhir.jpa.starter.AppProperties;
import ca.uhn.fhir.jpa.starter.util.EnvironmentHelper;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class OnPartitionModeEnabled implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
var appProperties = EnvironmentHelper.getConfiguration(context, "hapi.fhir", AppProperties.class);
if (appProperties == null) return false;
return appProperties.getPartitioning() != null;
}
}

View File

@@ -1,6 +1,5 @@
package ca.uhn.fhir.jpa.starter.common;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.interceptor.PatientIdPartitionInterceptor;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.partition.PartitionManagementProvider;
@@ -9,42 +8,31 @@ import ca.uhn.fhir.jpa.starter.AppProperties;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor;
import ca.uhn.fhir.rest.server.tenant.UrlBaseTenantIdentificationStrategy;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
@Component
@Conditional({OnPartitionModeEnabled.class})
public class PartitionModeConfigurer {
private static final Logger ourLog = LoggerFactory.getLogger(PartitionModeConfigurer.class);
@Autowired
private AppProperties myAppProperties;
public PartitionModeConfigurer(
AppProperties myAppProperties,
ISearchParamExtractor mySearchParamExtractor,
PartitionSettings myPartitionSettings,
RestfulServer myRestfulServer,
PartitionManagementProvider myPartitionManagementProvider) {
@Autowired
private FhirContext myFhirContext;
@Autowired
private ISearchParamExtractor mySearchParamExtractor;
@Autowired
private PartitionSettings myPartitionSettings;
@Autowired
private RestfulServer myRestfulServer;
@Autowired
private PartitionManagementProvider myPartitionManagementProvider;
@PostConstruct
public void start() {
if (myAppProperties.getPartitioning() != null) {
if (myAppProperties.getPartitioning().getPatient_id_partitioning_mode() == Boolean.TRUE) {
var partitioning = myAppProperties.getPartitioning();
if (partitioning.getPatient_id_partitioning_mode()) {
ourLog.info("Partitioning mode enabled in: Patient ID partitioning mode");
PatientIdPartitionInterceptor patientIdInterceptor =
new PatientIdPartitionInterceptor(myFhirContext, mySearchParamExtractor, myPartitionSettings);
var patientIdInterceptor = new PatientIdPartitionInterceptor(
myRestfulServer.getFhirContext(), mySearchParamExtractor, myPartitionSettings);
myRestfulServer.registerInterceptor(patientIdInterceptor);
myPartitionSettings.setUnnamedPartitionMode(true);
} else if (myAppProperties.getPartitioning().getRequest_tenant_partitioning_mode() == Boolean.TRUE) {
} else if (partitioning.getRequest_tenant_partitioning_mode()) {
ourLog.info("Partitioning mode enabled in: Request tenant partitioning mode");
myRestfulServer.registerInterceptor(new RequestTenantPartitionInterceptor());
myRestfulServer.setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy());
@@ -53,4 +41,3 @@ public class PartitionModeConfigurer {
myRestfulServer.registerProviders(myPartitionManagementProvider);
}
}
}

View File

@@ -9,6 +9,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.IDaoRegistry;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.config.ThreadPoolFactoryConfig;
@@ -20,6 +21,7 @@ import ca.uhn.fhir.jpa.config.util.HapiEntityManagerFactoryUtil;
import ca.uhn.fhir.jpa.config.util.ResourceCountCacheUtil;
import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.dao.TransactionProcessor;
import ca.uhn.fhir.jpa.dao.search.HSearchSortHelperImpl;
import ca.uhn.fhir.jpa.dao.search.IHSearchSortHelper;
import ca.uhn.fhir.jpa.delete.ThreadSafeResourceDeleterSvc;
@@ -29,8 +31,9 @@ import ca.uhn.fhir.jpa.interceptor.UserRequestRetryVersionConflictsInterceptor;
import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingInterceptor;
import ca.uhn.fhir.jpa.ips.provider.IpsOperationProvider;
import ca.uhn.fhir.jpa.model.config.SubscriptionSettings;
import ca.uhn.fhir.jpa.packages.AdditionalResourcesParser;
import ca.uhn.fhir.jpa.packages.IHapiPackageCacheManager;
import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc;
import ca.uhn.fhir.jpa.packages.PackageInstallationSpec;
import ca.uhn.fhir.jpa.provider.DaoRegistryResourceSupportedSvc;
import ca.uhn.fhir.jpa.provider.DiffProvider;
import ca.uhn.fhir.jpa.provider.IJpaSystemProvider;
@@ -47,14 +50,15 @@ import ca.uhn.fhir.jpa.starter.AppProperties;
import ca.uhn.fhir.jpa.starter.annotations.OnCorsPresent;
import ca.uhn.fhir.jpa.starter.annotations.OnImplementationGuidesPresent;
import ca.uhn.fhir.jpa.starter.common.validation.IRepositoryValidationInterceptorFactory;
import ca.uhn.fhir.jpa.starter.ig.ExtendedPackageInstallationSpec;
import ca.uhn.fhir.jpa.starter.ig.IImplementationGuideOperationProvider;
import ca.uhn.fhir.jpa.starter.util.EnvironmentHelper;
import ca.uhn.fhir.jpa.subscription.util.SubscriptionDebugLogInterceptor;
import ca.uhn.fhir.jpa.util.ResourceCountCache;
import ca.uhn.fhir.mdm.provider.MdmProviderLoader;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.narrative2.NullNarrativeGenerator;
import ca.uhn.fhir.rest.api.IResourceSupportedSvc;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.openapi.OpenApiInterceptor;
import ca.uhn.fhir.rest.server.ApacheProxyAddressStrategy;
import ca.uhn.fhir.rest.server.ETagSupportEnum;
@@ -74,12 +78,17 @@ import ca.uhn.fhir.validation.IValidatorModule;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import com.google.common.base.Strings;
import jakarta.persistence.EntityManagerFactory;
import org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
@@ -87,17 +96,12 @@ import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.http.HttpHeaders;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.web.cors.CorsConfiguration;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import javax.sql.DataSource;
import static ca.uhn.fhir.jpa.starter.common.validation.IRepositoryValidationInterceptorFactory.ENABLE_REPOSITORY_VALIDATING_INTERCEPTOR;
@@ -120,9 +124,6 @@ public class StarterJpaConfig {
return new StaleSearchDeletingSvcImpl();
}
@Autowired
private ConfigurableEnvironment configurableEnvironment;
/**
* Customize the default/max page sizes for search results. You can set these however
* you want, although very large page sizes will require a lot of RAM.
@@ -148,22 +149,46 @@ public class StarterJpaConfig {
@Primary
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
JpaProperties theJpaProperties,
DataSource myDataSource,
ConfigurableListableBeanFactory myConfigurableListableBeanFactory,
FhirContext theFhirContext,
JpaStorageSettings theStorageSettings) {
LocalContainerEntityManagerFactoryBean retVal = HapiEntityManagerFactoryUtil.newEntityManagerFactory(
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean =
HapiEntityManagerFactoryUtil.newEntityManagerFactory(
myConfigurableListableBeanFactory, theFhirContext, theStorageSettings);
retVal.setPersistenceUnitName("HAPI_PU");
try {
retVal.setDataSource(myDataSource);
} catch (Exception e) {
throw new ConfigurationException("Could not set the data source due to a configuration issue", e);
}
retVal.setJpaProperties(
EnvironmentHelper.getHibernateProperties(configurableEnvironment, myConfigurableListableBeanFactory));
return retVal;
// Spring Boot Autoconfiguration defaults
theJpaProperties
.getProperties()
.putIfAbsent(AvailableSettings.SCANNER, "org.hibernate.boot.archive.scan.internal.DisabledScanner");
theJpaProperties
.getProperties()
.putIfAbsent(AvailableSettings.IMPLICIT_NAMING_STRATEGY, SpringImplicitNamingStrategy.class.getName());
theJpaProperties
.getProperties()
.putIfAbsent(
AvailableSettings.PHYSICAL_NAMING_STRATEGY,
CamelCaseToUnderscoresNamingStrategy.class.getName());
// Hibernate Search defaults
theJpaProperties.getProperties().putIfAbsent(AvailableSettings.FORMAT_SQL, "false");
theJpaProperties.getProperties().putIfAbsent(AvailableSettings.SHOW_SQL, "false");
theJpaProperties.getProperties().putIfAbsent(AvailableSettings.HBM2DDL_AUTO, "update");
theJpaProperties.getProperties().putIfAbsent(AvailableSettings.STATEMENT_BATCH_SIZE, "20");
theJpaProperties.getProperties().putIfAbsent(AvailableSettings.USE_QUERY_CACHE, "false");
theJpaProperties.getProperties().putIfAbsent(AvailableSettings.USE_SECOND_LEVEL_CACHE, "false");
theJpaProperties.getProperties().putIfAbsent(AvailableSettings.USE_STRUCTURED_CACHE, "false");
theJpaProperties.getProperties().putIfAbsent(AvailableSettings.USE_MINIMAL_PUTS, "false");
// Hibernate Search defaults
theJpaProperties.getProperties().putIfAbsent(HibernateOrmMapperSettings.ENABLED, "false");
entityManagerFactoryBean.setPersistenceUnitName("HAPI_PU");
entityManagerFactoryBean.setJpaPropertyMap(theJpaProperties.getProperties());
entityManagerFactoryBean.setDataSource(myDataSource);
return entityManagerFactoryBean;
}
@Bean
@@ -207,14 +232,17 @@ public class StarterJpaConfig {
public IPackageInstallerSvc packageInstaller(
AppProperties appProperties,
IPackageInstallerSvc packageInstallerSvc,
Batch2JobRegisterer batch2JobRegisterer) {
Batch2JobRegisterer batch2JobRegisterer,
FhirContext fhirContext,
TransactionProcessor transactionProcessor,
IHapiPackageCacheManager iHapiPackageCacheManager) {
batch2JobRegisterer.start();
if (appProperties.getImplementationGuides() != null) {
Map<String, PackageInstallationSpec> guides = appProperties.getImplementationGuides();
for (Map.Entry<String, PackageInstallationSpec> guidesEntry : guides.entrySet()) {
PackageInstallationSpec packageInstallationSpec = guidesEntry.getValue();
Map<String, ExtendedPackageInstallationSpec> guides = appProperties.getImplementationGuides();
for (Map.Entry<String, ExtendedPackageInstallationSpec> guidesEntry : guides.entrySet()) {
ExtendedPackageInstallationSpec packageInstallationSpec = guidesEntry.getValue();
if (appProperties.getInstall_transitive_ig_dependencies()) {
packageInstallationSpec
@@ -223,7 +251,22 @@ public class StarterJpaConfig {
.addDependencyExclude("hl7.fhir.r4.core")
.addDependencyExclude("hl7.fhir.r5.core");
}
packageInstallerSvc.install(packageInstallationSpec);
Set<String> extraResources = packageInstallationSpec.getAdditionalResourceFolders();
packageInstallationSpec.setPackageContents(iHapiPackageCacheManager
.loadPackageContents(packageInstallationSpec.getName(), packageInstallationSpec.getVersion())
.getBytes());
if (extraResources != null && !extraResources.isEmpty()) {
IBaseBundle transaction = AdditionalResourcesParser.bundleAdditionalResources(
extraResources, packageInstallationSpec, fhirContext);
transactionProcessor.transaction(
new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.defaultPartition()),
transaction,
false);
}
}
}
return packageInstallerSvc;

View File

@@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.starter.common.validation;
import ca.uhn.fhir.jpa.starter.AppProperties;
import org.springframework.boot.context.properties.bind.Binder;
import ca.uhn.fhir.jpa.starter.util.EnvironmentHelper;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
@@ -10,9 +10,8 @@ public class OnRemoteTerminologyPresent implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) {
AppProperties config = Binder.get(conditionContext.getEnvironment())
.bind("hapi.fhir", AppProperties.class)
.orElse(null);
AppProperties config = EnvironmentHelper.getConfiguration(conditionContext, "hapi.fhir", AppProperties.class);
if (config == null) return false;
if (config.getRemoteTerminologyServicesMap() == null) return false;
return !config.getRemoteTerminologyServicesMap().isEmpty();

View File

@@ -1,5 +1,10 @@
package ca.uhn.fhir.jpa.starter.cr;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "hapi.fhir.cr.caregaps")
public class CareGapsProperties {
private String reporter = "default";
private String section_author = "default";

View File

@@ -3,7 +3,11 @@ package ca.uhn.fhir.jpa.starter.cr;
import org.cqframework.cql.cql2elm.CqlCompilerException;
import org.cqframework.cql.cql2elm.CqlTranslator;
import org.cqframework.cql.cql2elm.LibraryBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "hapi.fhir.cr.cql.compiler")
public class CqlCompilerProperties {
private Boolean validate_units = true;
private Boolean verify_only = false;

View File

@@ -0,0 +1,46 @@
package ca.uhn.fhir.jpa.starter.cr;
import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "hapi.fhir.cr.cql.data")
public class CqlData {
private RetrieveSettings.SEARCH_FILTER_MODE searchParameterMode = RetrieveSettings.SEARCH_FILTER_MODE.AUTO;
private RetrieveSettings.PROFILE_MODE profileMode = RetrieveSettings.PROFILE_MODE.OFF;
private RetrieveSettings.TERMINOLOGY_FILTER_MODE terminologyParameterMode =
RetrieveSettings.TERMINOLOGY_FILTER_MODE.AUTO;
public RetrieveSettings.SEARCH_FILTER_MODE getSearchParameterMode() {
return searchParameterMode;
}
public void setSearchParameterMode(RetrieveSettings.SEARCH_FILTER_MODE searchParameterMode) {
this.searchParameterMode = searchParameterMode;
}
public RetrieveSettings.PROFILE_MODE getProfileMode() {
return profileMode;
}
public void setProfileMode(RetrieveSettings.PROFILE_MODE profileMode) {
this.profileMode = profileMode;
}
public RetrieveSettings.TERMINOLOGY_FILTER_MODE getTerminologyParameterMode() {
return terminologyParameterMode;
}
public void setTerminologyParameterMode(RetrieveSettings.TERMINOLOGY_FILTER_MODE terminologyParameterMode) {
this.terminologyParameterMode = terminologyParameterMode;
}
public RetrieveSettings getRetrieveSettings() {
var retrieveSettings = new RetrieveSettings();
retrieveSettings.setSearchParameterMode(searchParameterMode);
retrieveSettings.setProfileMode(profileMode);
retrieveSettings.setTerminologyParameterMode(terminologyParameterMode);
return retrieveSettings;
}
}

View File

@@ -2,14 +2,18 @@ package ca.uhn.fhir.jpa.starter.cr;
import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings;
import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "hapi.fhir.cr.cql")
public class CqlProperties {
private Boolean use_embedded_libraries = true;
private CqlCompilerProperties compiler = new CqlCompilerProperties();
private CqlRuntimeProperties runtime = new CqlRuntimeProperties();
private TerminologySettings terminology = new TerminologySettings();
private RetrieveSettings data = new RetrieveSettings();
private CqlData data = new CqlData();
public Boolean getUse_embedded_libraries() {
return use_embedded_libraries;
@@ -43,11 +47,15 @@ public class CqlProperties {
this.terminology = terminology;
}
public RetrieveSettings getData() {
public CqlData getData() {
return data;
}
public void setData(RetrieveSettings data) {
public void setData(CqlData data) {
this.data = data;
}
public RetrieveSettings getRetrieveSettings() {
return data.getRetrieveSettings();
}
}

View File

@@ -1,5 +1,10 @@
package ca.uhn.fhir.jpa.starter.cr;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "hapi.fhir.cr.cql.runtime")
public class CqlRuntimeProperties {
private Boolean debug_logging_enabled = false;

View File

@@ -0,0 +1,43 @@
package ca.uhn.fhir.jpa.starter.cr;
import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@ConfigurationProperties(prefix = "hapi.fhir.cr.cql.terminology")
@Configuration
public class CqlTerminologyProperties {
private TerminologySettings.VALUESET_EXPANSION_MODE valuesetExpansionMode =
TerminologySettings.VALUESET_EXPANSION_MODE.AUTO;
private TerminologySettings.VALUESET_MEMBERSHIP_MODE valuesetMembershipMode =
TerminologySettings.VALUESET_MEMBERSHIP_MODE.AUTO;
private TerminologySettings.CODE_LOOKUP_MODE codeLookupMode = TerminologySettings.CODE_LOOKUP_MODE.AUTO;
private TerminologySettings.VALUESET_PRE_EXPANSION_MODE valueSetPreExpansionMode =
TerminologySettings.VALUESET_PRE_EXPANSION_MODE.USE_IF_PRESENT;
public void setValuesetExpansionMode(TerminologySettings.VALUESET_EXPANSION_MODE valuesetExpansionMode) {
this.valuesetExpansionMode = valuesetExpansionMode;
}
public void setValuesetMembershipMode(TerminologySettings.VALUESET_MEMBERSHIP_MODE valuesetMembershipMode) {
this.valuesetMembershipMode = valuesetMembershipMode;
}
public void setCodeLookupMode(TerminologySettings.CODE_LOOKUP_MODE codeLookupMode) {
this.codeLookupMode = codeLookupMode;
}
public void setValueSetPreExpansionMode(TerminologySettings.VALUESET_PRE_EXPANSION_MODE valueSetPreExpansionMode) {
this.valueSetPreExpansionMode = valueSetPreExpansionMode;
}
public TerminologySettings getTerminologySettings() {
TerminologySettings settings = new TerminologySettings();
settings.setValuesetExpansionMode(valuesetExpansionMode);
settings.setValuesetMembershipMode(valuesetMembershipMode);
settings.setCodeLookupMode(codeLookupMode);
settings.setValuesetPreExpansionMode(valueSetPreExpansionMode);
return settings;
}
}

View File

@@ -23,7 +23,6 @@ import org.opencds.cqf.fhir.cr.measure.CareGapsProperties;
import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions;
import org.opencds.cqf.fhir.utility.ValidationProfile;
import org.opencds.cqf.fhir.utility.client.TerminologyServerClientSettings;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@@ -43,19 +42,13 @@ import java.util.concurrent.Executors;
public class CrCommonConfig {
@Bean
@ConfigurationProperties(prefix = "hapi.fhir.cr")
CrProperties crProperties() {
return new CrProperties();
RetrieveSettings retrieveSettings(CqlData cqlData) {
return cqlData.getRetrieveSettings();
}
@Bean
RetrieveSettings retrieveSettings(CrProperties theCrProperties) {
return theCrProperties.getCql().getData();
}
@Bean
TerminologySettings terminologySettings(CrProperties theCrProperties) {
return theCrProperties.getCql().getTerminology();
TerminologySettings terminologySettings(CqlTerminologyProperties theCqlTerminologyProperties) {
return theCqlTerminologyProperties.getTerminologySettings();
}
@Bean
@@ -65,7 +58,8 @@ public class CrCommonConfig {
@Bean
public EvaluationSettings evaluationSettings(
CrProperties theCrProperties,
CqlRuntimeProperties cqlRuntimeProperties,
CqlCompilerProperties cqlCompilerProperties,
RetrieveSettings theRetrieveSettings,
TerminologySettings theTerminologySettings,
Map<VersionedIdentifier, CompiledLibrary> theGlobalLibraryCache,
@@ -76,7 +70,7 @@ public class CrCommonConfig {
var cqlEngineOptions = cqlOptions.getCqlEngineOptions();
Set<CqlEngine.Options> options = EnumSet.noneOf(CqlEngine.Options.class);
var cqlRuntimeProperties = theCrProperties.getCql().getRuntime();
if (cqlRuntimeProperties.isEnableExpressionCaching()) {
options.add(CqlEngine.Options.EnableExpressionCaching);
}
@@ -91,8 +85,6 @@ public class CrCommonConfig {
var cqlCompilerOptions = new CqlCompilerOptions();
var cqlCompilerProperties = theCrProperties.getCql().getCompiler();
if (cqlCompilerProperties.isEnableDateRangeOptimization()) {
cqlCompilerOptions.setOptions(CqlCompilerOptions.Options.EnableDateRangeOptimization);
}
@@ -159,8 +151,8 @@ public class CrCommonConfig {
return executor;
}
@Bean
CareGapsProperties careGapsProperties(CrProperties theCrProperties) {
@Bean(name = "measure.CareGapsProperties")
org.opencds.cqf.fhir.cr.measure.CareGapsProperties careGapsProperties(CrProperties theCrProperties) {
var careGapsProperties = new CareGapsProperties();
// This check for the resource type really should be happening down in CR where the setting is actually used but
// that will have to wait for a future CR release

View File

@@ -1,14 +1,18 @@
package ca.uhn.fhir.jpa.starter.cr;
import org.opencds.cqf.fhir.utility.client.TerminologyServerClientSettings;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "hapi.fhir.cr")
public class CrProperties {
private Boolean enabled;
private CareGapsProperties careGaps = new CareGapsProperties();
private CqlProperties cql = new CqlProperties();
private TerminologyServerClientSettings terminologyServerClientSettings = new TerminologyServerClientSettings();
private TerminologyServerClientSettings terminologyServerClientSettings =
TerminologyServerClientSettings.getDefault();
public Boolean getEnabled() {
return enabled;

View File

@@ -0,0 +1,17 @@
package ca.uhn.fhir.jpa.starter.elastic;
import ca.uhn.fhir.jpa.starter.util.EnvironmentHelper;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchProperties;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class ElasticConfigCondition implements Condition {
@Override
public boolean matches(ConditionContext theConditionContext, AnnotatedTypeMetadata theAnnotatedTypeMetadata) {
return EnvironmentHelper.getConfiguration(
theConditionContext, "spring.elasticsearch", ElasticsearchProperties.class)
!= null;
}
}

View File

@@ -0,0 +1,163 @@
package ca.uhn.fhir.jpa.starter.elastic;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.dao.TolerantJsonParser;
import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl;
import ca.uhn.fhir.jpa.search.lastn.IElasticsearchSvc;
import ca.uhn.fhir.jpa.search.lastn.json.ObservationJson;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.indices.ExistsRequest;
import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Conditional(ElasticConfigCondition.class)
public class ElasticsearchBootSvcImpl implements IElasticsearchSvc {
// Index Constants
public static final String OBSERVATION_INDEX = "observation_index";
public static final String OBSERVATION_CODE_INDEX = "code_index";
public static final String OBSERVATION_INDEX_SCHEMA_FILE = "ObservationIndexSchema.json";
public static final String OBSERVATION_CODE_INDEX_SCHEMA_FILE = "ObservationCodeIndexSchema.json";
// Aggregation Constants
// Observation index document element names
private static final String OBSERVATION_IDENTIFIER_FIELD_NAME = "identifier";
// Code index document element names
private static final String CODE_HASH = "codingcode_system_hash";
private static final String CODE_TEXT = "text";
private static final String OBSERVATION_RESOURCE_NAME = "Observation";
private final ElasticsearchClient myRestHighLevelClient;
private final FhirContext myContext;
public ElasticsearchBootSvcImpl(ElasticsearchClient client, FhirContext fhirContext) {
myContext = fhirContext;
myRestHighLevelClient = client;
try {
createObservationIndexIfMissing();
createObservationCodeIndexIfMissing();
} catch (IOException theE) {
throw new RuntimeException(Msg.code(1175) + "Failed to create document index", theE);
}
}
private String getIndexSchema(String theSchemaFileName) throws IOException {
InputStreamReader input =
new InputStreamReader(ElasticsearchSvcImpl.class.getResourceAsStream(theSchemaFileName));
BufferedReader reader = new BufferedReader(input);
StringBuilder sb = new StringBuilder();
String str;
while ((str = reader.readLine()) != null) {
sb.append(str);
}
return sb.toString();
}
private void createObservationIndexIfMissing() throws IOException {
if (indexExists(OBSERVATION_INDEX)) {
return;
}
String observationMapping = getIndexSchema(OBSERVATION_INDEX_SCHEMA_FILE);
if (!createIndex(OBSERVATION_INDEX, observationMapping)) {
throw new RuntimeException(Msg.code(1176) + "Failed to create observation index");
}
}
private void createObservationCodeIndexIfMissing() throws IOException {
if (indexExists(OBSERVATION_CODE_INDEX)) {
return;
}
String observationCodeMapping = getIndexSchema(OBSERVATION_CODE_INDEX_SCHEMA_FILE);
if (!createIndex(OBSERVATION_CODE_INDEX, observationCodeMapping)) {
throw new RuntimeException(Msg.code(1177) + "Failed to create observation code index");
}
}
private boolean createIndex(String theIndexName, String theMapping) throws IOException {
return myRestHighLevelClient
.indices()
.create(cir -> cir.index(theIndexName).withJson(new StringReader(theMapping)))
.acknowledged();
}
private boolean indexExists(String theIndexName) throws IOException {
ExistsRequest request = new ExistsRequest.Builder().index(theIndexName).build();
return myRestHighLevelClient.indices().exists(request).value();
}
@Override
public void close() {
// nothing
}
@Override
public List<IBaseResource> getObservationResources(Collection<? extends IResourcePersistentId> thePids) {
SearchRequest searchRequest = buildObservationResourceSearchRequest(thePids);
try {
SearchResponse<ObservationJson> observationDocumentResponse =
myRestHighLevelClient.search(searchRequest, ObservationJson.class);
List<Hit<ObservationJson>> observationDocumentHits =
observationDocumentResponse.hits().hits();
IParser parser = TolerantJsonParser.createWithLenientErrorHandling(myContext, null);
Class<? extends IBaseResource> resourceType =
myContext.getResourceDefinition(OBSERVATION_RESOURCE_NAME).getImplementingClass();
/**
* @see ca.uhn.fhir.jpa.dao.BaseHapiFhirDao#toResource(Class, IBaseResourceEntity, Collection, boolean) for
* details about parsing raw json to BaseResource
*/
return observationDocumentHits.stream()
.map(Hit::source)
.map(observationJson -> parser.parseResource(resourceType, observationJson.getResource()))
.collect(Collectors.toList());
} catch (IOException theE) {
throw new InvalidRequestException(
Msg.code(2003) + "Unable to execute observation document query for provided IDs " + thePids, theE);
}
}
private SearchRequest buildObservationResourceSearchRequest(Collection<? extends IResourcePersistentId> thePids) {
List<FieldValue> values = thePids.stream()
.map(Object::toString)
.map(v -> FieldValue.of(v))
.collect(Collectors.toList());
return SearchRequest.of(sr -> sr.index(OBSERVATION_INDEX)
.query(qb -> qb.bool(bb -> bb.must(bbm -> {
bbm.terms(terms ->
terms.field(OBSERVATION_IDENTIFIER_FIELD_NAME).terms(termsb -> termsb.value(values)));
return bbm;
})))
.size(thePids.size()));
}
@VisibleForTesting
public void refreshIndex(String theIndexName) throws IOException {
myRestHighLevelClient.indices().refresh(fn -> fn.index(theIndexName));
}
}

View File

@@ -0,0 +1,24 @@
package ca.uhn.fhir.jpa.starter.ig;
import ca.uhn.fhir.jpa.packages.PackageInstallationSpec;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Set;
public class ExtendedPackageInstallationSpec extends PackageInstallationSpec {
public Set<String> getAdditionalResourceFolders() {
return additionalResourceFolders;
}
public void setAdditionalResourceFolders(Set<String> additionalResourceFolders) {
this.additionalResourceFolders = additionalResourceFolders;
}
@Schema(
description =
"Specifies folder names containing additional resources to load. These folders will be scanned for resources to include during installation.")
@JsonProperty("additionalResourceFolders")
private Set<String> additionalResourceFolders;
}

View File

@@ -1,14 +0,0 @@
package ca.uhn.fhir.jpa.starter.ig;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class IgConfigCondition implements Condition {
@Override
public boolean matches(ConditionContext theConditionContext, AnnotatedTypeMetadata theAnnotatedTypeMetadata) {
String property = theConditionContext.getEnvironment().getProperty("hapi.fhir.ig_runtime_upload_enabled");
return Boolean.parseBoolean(property);
}
}

View File

@@ -7,16 +7,18 @@ import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import org.hl7.fhir.r4.model.Base64BinaryType;
import org.hl7.fhir.r4.model.Parameters;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Conditional({OnR4Condition.class, IgConfigCondition.class})
@Conditional({OnR4Condition.class})
@ConditionalOnProperty(name = "hapi.fhir.ig_runtime_upload_enabled", havingValue = "true")
@Service
public class ImplementationGuideR4OperationProvider implements IImplementationGuideOperationProvider {
IPackageInstallerSvc packageInstallerSvc;
final IPackageInstallerSvc packageInstallerSvc;
public ImplementationGuideR4OperationProvider(IPackageInstallerSvc packageInstallerSvc) {
this.packageInstallerSvc = packageInstallerSvc;

View File

@@ -7,16 +7,18 @@ import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import org.hl7.fhir.r5.model.Base64BinaryType;
import org.hl7.fhir.r5.model.Parameters;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Conditional({OnR5Condition.class, IgConfigCondition.class})
@Conditional({OnR5Condition.class})
@ConditionalOnProperty(name = "hapi.fhir.ig_runtime_upload_enabled", havingValue = "true")
@Service
public class ImplementationGuideR5OperationProvider implements IImplementationGuideOperationProvider {
IPackageInstallerSvc packageInstallerSvc;
final IPackageInstallerSvc packageInstallerSvc;
public ImplementationGuideR5OperationProvider(IPackageInstallerSvc packageInstallerSvc) {
this.packageInstallerSvc = packageInstallerSvc;

View File

@@ -1,14 +0,0 @@
package ca.uhn.fhir.jpa.starter.ips;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class IpsConfigCondition implements Condition {
@Override
public boolean matches(ConditionContext theConditionContext, AnnotatedTypeMetadata theAnnotatedTypeMetadata) {
String property = theConditionContext.getEnvironment().getProperty("hapi.fhir.ips_enabled");
return Boolean.parseBoolean(property);
}
}

View File

@@ -6,10 +6,12 @@ import ca.uhn.fhir.jpa.ips.generator.IIpsGeneratorSvc;
import ca.uhn.fhir.jpa.ips.generator.IpsGeneratorSvcImpl;
import ca.uhn.fhir.jpa.ips.jpa.DefaultJpaIpsGenerationStrategy;
import ca.uhn.fhir.jpa.ips.provider.IpsOperationProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@Conditional(IpsConfigCondition.class)
@Configuration
@ConditionalOnProperty(name = "hapi.fhir.ips_enabled", havingValue = "true")
public class StarterIpsConfig {
@Bean
IIpsGenerationStrategy ipsGenerationStrategy() {

View File

@@ -0,0 +1,40 @@
package ca.uhn.fhir.jpa.starter.mcp;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class CallToolResultFactory {
public static McpSchema.CallToolResult success(
String resourceType, Interaction interaction, String response, int status) {
Map<String, Object> payload = Map.of(
"resourceType", resourceType,
"interaction", interaction,
"response", response,
"status", status);
ObjectMapper objectMapper = new ObjectMapper();
String jacksonData;
try {
jacksonData = objectMapper.writeValueAsString(payload);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
return McpSchema.CallToolResult.builder()
.addContent(new McpSchema.TextContent(jacksonData))
.build();
}
public static McpSchema.CallToolResult failure(String message) {
return McpSchema.CallToolResult.builder()
.isError(true)
.addTextContent(message)
.build();
}
}

View File

@@ -0,0 +1,34 @@
package ca.uhn.fhir.jpa.starter.mcp;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
public enum Interaction {
CALL_CDS_HOOK("call-cds-hook"),
SEARCH("search"),
READ("read"),
CREATE("create"),
UPDATE("update"),
DELETE("delete"),
PATCH("patch"),
TRANSACTION("transaction");
private final String name;
Interaction(String name) {
this.name = name;
}
public String getName() {
return name;
}
public RequestTypeEnum asRequestType() {
return switch (this) {
case SEARCH, READ -> RequestTypeEnum.GET;
case CREATE, TRANSACTION, CALL_CDS_HOOK -> RequestTypeEnum.POST;
case UPDATE -> RequestTypeEnum.PUT;
case DELETE -> RequestTypeEnum.DELETE;
case PATCH -> RequestTypeEnum.PATCH;
};
}
}

View File

@@ -0,0 +1,81 @@
package ca.uhn.fhir.jpa.starter.mcp;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.McpBridge;
import ca.uhn.fhir.rest.server.McpCdsBridge;
import ca.uhn.fhir.rest.server.McpFhirBridge;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceRegistry;
import ca.uhn.hapi.fhir.cdshooks.module.CdsHooksObjectMapperFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import java.util.List;
// https://mcp-cn.ssshooter.com/sdk/java/mcp-server#sse-servlet
// https://www.baeldung.com/spring-ai-model-context-protocol-mcp
// https://github.com/spring-projects/spring-ai-examples/blob/main/model-context-protocol/weather/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerConfig.java
// https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-stdio-server/src/main/java/org/springframework/ai/mcp/sample/server
// https://github.com/spring-projects/spring-ai-examples/blob/main/model-context-protocol/sampling/mcp-weather-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java
// https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html
@Configuration
@ConditionalOnProperty(
prefix = "spring.ai.mcp.server",
name = {"enabled"},
havingValue = "true")
@Import(McpServerStreamableHttpProperties.class)
public class McpServerConfig {
private static final String SSE_ENDPOINT = "/sse";
private static final String SSE_MESSAGE_ENDPOINT = "/mcp/message";
@Bean
public List<McpServerFeatures.SyncToolSpecification> syncServer(List<McpBridge> mcpBridges) {
return mcpBridges.stream()
.flatMap(bridge -> bridge.generateTools().stream())
.toList();
}
@Bean
public McpFhirBridge mcpFhirBridge(RestfulServer restfulServer) {
return new McpFhirBridge(restfulServer);
}
@Bean
@ConditionalOnProperty(
prefix = "hapi.fhir.cdshooks",
name = {"enabled"},
havingValue = "true")
public McpCdsBridge mcpCdsBridge(FhirContext fhirContext, ICdsServiceRegistry cdsServiceRegistry) {
return new McpCdsBridge(
fhirContext, cdsServiceRegistry, new CdsHooksObjectMapperFactory(fhirContext).newMapper());
}
@Bean
public HttpServletStreamableServerTransportProvider servletSseServerTransportProvider(
McpServerStreamableHttpProperties properties) {
return HttpServletStreamableServerTransportProvider.builder()
.disallowDelete(false)
.mcpEndpoint(properties.getMcpEndpoint())
.jsonMapper(new JacksonMcpJsonMapper(new ObjectMapper()))
// .contextExtractor((serverRequest, context) -> context)
.build();
}
@Bean
public ServletRegistrationBean customServletBean(
HttpServletStreamableServerTransportProvider transportProvider,
McpServerStreamableHttpProperties properties) {
return new ServletRegistrationBean<>(transportProvider, properties.getMcpEndpoint(), SSE_ENDPOINT);
}
}

View File

@@ -0,0 +1,120 @@
package ca.uhn.fhir.jpa.starter.mcp;
import ca.uhn.fhir.context.FhirContext;
import com.google.gson.Gson;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.mock.web.MockHttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class RequestBuilder {
private final FhirContext fhirContext;
private final String resourceType;
private final Interaction interaction;
private final Map<String, Object> config;
/**
* Constructs a RequestBuilder for a specific FHIR interaction.
*
* @param fhirContext the FHIR context
* @param contextMap a map containing configuration parameters, including 'resourceType'
* @param interaction the type of interaction (e.g., SEARCH, READ, CREATE, etc.)
*/
public RequestBuilder(FhirContext fhirContext, Map<String, Object> contextMap, Interaction interaction) {
this.config = contextMap;
if (interaction == Interaction.TRANSACTION) this.resourceType = "";
else if (contextMap.get("resourceType") instanceof String rt && !rt.isBlank()) this.resourceType = rt;
else throw new IllegalArgumentException("Missing or invalid 'resourceType' in contextMap");
this.interaction = interaction;
this.fhirContext = fhirContext;
}
public MockHttpServletRequest buildRequest() {
String basePath = "/" + resourceType;
String method;
MockHttpServletRequest req;
switch (interaction) {
case SEARCH -> {
method = "GET";
req = new MockHttpServletRequest(method, basePath);
Map<?, ?> sp = null;
if (config.get("query") instanceof Map<?, ?> q) {
sp = q;
} else if (config.get("searchParams") instanceof Map<?, ?> s) {
sp = s;
}
if (sp != null) {
sp.forEach((k, v) -> req.addParameter(k.toString(), v.toString()));
}
}
case READ -> {
method = "GET";
String id = requireString();
req = new MockHttpServletRequest(method, basePath + "/" + id);
}
case CREATE, TRANSACTION -> {
method = "POST";
req = new MockHttpServletRequest(method, basePath);
applyResourceBody(req);
}
case UPDATE -> {
method = "PUT";
String id = requireString();
req = new MockHttpServletRequest(method, basePath + "/" + id);
applyResourceBody(req);
}
case DELETE -> {
method = "DELETE";
String id = requireString();
req = new MockHttpServletRequest(method, basePath + "/" + id);
}
case PATCH -> {
method = "PATCH";
String id = requireString();
req = new MockHttpServletRequest(method, basePath + "/" + id);
applyPatchBody(req);
}
default -> throw new IllegalArgumentException("Unsupported interaction: " + interaction);
}
req.setContentType("application/fhir+json");
req.addHeader("Accept", "application/fhir+json");
return req;
}
private void applyResourceBody(MockHttpServletRequest req) {
Object resourceObj = config.get("resource");
String json;
if (resourceObj instanceof Map<?, ?>) json = new Gson().toJson(resourceObj, Map.class);
else if (resourceObj instanceof String) json = resourceObj.toString();
else throw new IllegalArgumentException("Unsupported resource body type: " + resourceObj.getClass());
req.setContent(json.getBytes(StandardCharsets.UTF_8));
}
private void applyPatchBody(MockHttpServletRequest req) {
Object patchBody = config.get("resource");
if (patchBody == null) {
throw new IllegalArgumentException("Missing 'resource' for patch interaction");
}
String content;
if (patchBody instanceof String s) {
content = s;
} else if (patchBody instanceof IBaseResource r) {
content = fhirContext.newJsonParser().encodeResourceToString(r);
} else {
throw new IllegalArgumentException("Unsupported patch body type: " + patchBody.getClass());
}
req.setContent(content.getBytes(StandardCharsets.UTF_8));
}
private String requireString() {
Object val = config.get("id");
if (!(val instanceof String s) || s.isBlank()) {
throw new IllegalArgumentException("Missing or invalid '" + "id" + "'");
}
return s;
}
}

View File

@@ -0,0 +1,337 @@
package ca.uhn.fhir.jpa.starter.mcp;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.Tool;
public class ToolFactory {
private static final String READ_FHIR_RESOURCE_SCHEMA =
"""
{
"type": "object",
"properties": {
"resourceType": {
"type": "string",
"description": "type of the resource to read"
},
"id": {
"type": "string",
"description": "id of the resource to read"
}
}
}
""";
private static final String CREATE_FHIR_RESOURCE_SCHEMA =
"""
{
"type": "object",
"properties": {
"resourceType": {
"type": "string",
"description": "Type of the resource to create"
},
"resource": {
"type": "object",
"description": "Resource content in JSON format"
},
"headers": {
"type": "object",
"description": "Headers for create request.\\nAvailable headers: If-None-Exist header for conditional create where the value is search param string.\\nFor example: {\\"If-None-Exist\\": \\"active=false\\"}"
}
},
"required": ["resourceType", "resource"]
}
""";
private static final String UPDATE_FHIR_RESOURCE_SCHEMA =
"""
{
"type": "object",
"properties": {
"resourceType": {
"type": "string",
"description": "Type of the resource to update"
},
"id": {
"type": "string",
"description": "ID of the resource to update"
},
"resource": {
"type": "object",
"description": "Updated resource content in JSON format"
}
},
"required": ["resourceType", "id", "resource"]
}
""";
private static final String CONDITIONAL_UPDATE_FHIR_RESOURCE_SCHEMA =
"""
{
"type": "object",
"properties": {
"resourceType": {
"type": "string",
"description": "Type of the resource to update"
},
"resource": {
"type": "object",
"description": "Updated resource content in JSON format"
},
"query": {
"type": "string",
"description": "Query string with search params separate by \\",\\". For example: \\"_id=pt-1,name=ivan\\". Uses for conditional update."
},
"headers": {
"type": "object",
"description": "Headers for create request.\\nAvailable headers: If-None-Match header for conditional update where the value is ETag.\\nFor example: {\\"If-None-Match\\": \\"12345\\"}"
}
},
"required": ["resourceType", "resource"]
}
""";
private static final String CONDITIONAL_PATCH_FHIR_RESOURCE_SCHEMA =
"""
{
"type": "object",
"properties": {
"resourceType": {
"type": "string",
"description": "Type of the resource to patch"
},
"resource": {
"type": "object",
"description": "Resource content to patch in JSON format"
},
"query": {
"type": "string",
"description": "Query string with search params separate by \\",\\". For example: \\"_id=pt-1,name=ivan\\". Uses for conditional patch."
},
"headers": {
"type": "object",
"description": "Headers for create request.\\nAvailable headers: If-None-Match header for conditional patch where the value is ETag.\\nFor example: {\\"If-None-Match\\": \\"12345\\"}"
}
},
"required": ["resourceType", "resource"]
}
""";
private static final String PATCH_FHIR_RESOURCE_SCHEMA =
"""
{
"type": "object",
"properties": {
"resourceType": {
"type": "string",
"description": "Type of the resource to patch"
},
"id": {
"type": "string",
"description": "ID of the resource to patch"
},
"resource": {
"type": "object",
"description": "Resource content to patch in JSON format"
}
},
"required": ["resourceType", "id", "resource"]
}
""";
private static final String DELETE_FHIR_RESOURCE_SCHEMA =
"""
{
"type": "object",
"properties": {
"resourceType": {
"type": "string",
"description": "Type of the resource to delete"
},
"id": {
"type": "string",
"description": "ID of the resource to delete"
}
},
"required": ["resourceType", "id"]
}
""";
private static final String SEARCH_FHIR_RESOURCES_SCHEMA =
"""
{
"type": "object",
"properties": {
"resourceType": {
"type": "string",
"description": "Type of the resource to search"
},
"query": {
"type": "string",
"description": "Query string with search params separate by \\",\\". For example: \\"_id=pt-1,name=ivan\\""
}
},
"required": ["resourceType", "query"]
}
""";
private static final String CREATE_FHIR_TRANSACTION_SCHEMA =
"""
{
"type": "object",
"properties": {
"resourceType": {
"type": "string",
"description": "A Bundle resource type with type 'transaction' containing multiple FHIR resources"
},
"resource": {
"type": "object",
"description": "A FHIR Bundle Resource content in JSON format"
}
},
"required": ["resourceType", "resource"]
}
""";
// TODO Add a tool for the CDS Hooks discovery endpoint
// Alternatively, should each service be a separate tool?
// TODO Add other fields from https://cds-hooks.hl7.org/STU2/#http-request-1
// TODO Context here is for the patient-view hook, https://cds-hooks.hl7.org/hooks/STU1/patient-view.html#context
private static final String CALL_CDS_HOOK_SCHEMA_2_0_1 =
"""
{
"type": "object",
"properties": {
"service": {
"type": "string",
"description": "The CDS Service to call."
},
"hook": {
"type": "string",
"description": "The hook that triggered this CDS Service call."
},
"hookInstance": {
"type": "string",
"description": "A universally unique identifier (UUID) for this particular hook call."
},
"hookContext": {
"type": "object",
"description": "Hook-specific contextual data that the CDS service will need.",
"properties": {
"userId": {
"type": "string",
"description": "The id of the current user. Must be in the format [ResourceType]/[id]."
},
"patientId": {
"type": "string",
"description": "The FHIR Patient.id of the current patient in context"
},
"encounterId": {
"type": "string",
"description": "The FHIR Encounter.id of the current encounter in context."
}
}
},
"prefetch": {
"type": "object",
"description": "Additional data to prefetch for the CDS service call."
}
},
"required": ["service", "hook", "hookInstance", "hookContext"]
}
""";
public static Tool readFhirResource() throws JsonProcessingException {
return new Tool.Builder()
.name("read-fhir-resource")
.description("Read an individual FHIR resource")
.inputSchema(mapper.readValue(READ_FHIR_RESOURCE_SCHEMA, McpSchema.JsonSchema.class))
.build();
}
public static Tool createFhirResource() throws JsonProcessingException {
return new Tool.Builder()
.name("create-fhir-resource")
.description("Create a new FHIR resource")
.inputSchema(mapper.readValue(CREATE_FHIR_RESOURCE_SCHEMA, McpSchema.JsonSchema.class))
.build();
}
public static Tool updateFhirResource() throws JsonProcessingException {
return new Tool.Builder()
.name("update-fhir-resource")
.description("Update an existing FHIR resource")
.inputSchema(mapper.readValue(UPDATE_FHIR_RESOURCE_SCHEMA, McpSchema.JsonSchema.class))
.build();
}
public static Tool conditionalUpdateFhirResource() throws JsonProcessingException {
return new Tool.Builder()
.name("conditional-update-fhir-resource")
.description("Conditional update an existing FHIR resource")
.inputSchema(mapper.readValue(CONDITIONAL_UPDATE_FHIR_RESOURCE_SCHEMA, McpSchema.JsonSchema.class))
.build();
}
public static Tool conditionalPatchFhirResource() throws JsonProcessingException {
return new Tool.Builder()
.name("conditional-patch-fhir-resource")
.description("Conditional patch an existing FHIR resource")
.inputSchema(mapper.readValue(CONDITIONAL_PATCH_FHIR_RESOURCE_SCHEMA, McpSchema.JsonSchema.class))
.build();
}
public static Tool patchFhirResource() throws JsonProcessingException {
return new Tool.Builder()
.name("patch-fhir-resource")
.description("Patch an existing FHIR resource")
.inputSchema(mapper.readValue(PATCH_FHIR_RESOURCE_SCHEMA, McpSchema.JsonSchema.class))
.build();
}
public static Tool deleteFhirResource() throws JsonProcessingException {
return new Tool.Builder()
.name("delete-fhir-resource")
.description("Delete an existing FHIR resource")
.inputSchema(mapper.readValue(DELETE_FHIR_RESOURCE_SCHEMA, McpSchema.JsonSchema.class))
.build();
}
public static Tool searchFhirResources() throws JsonProcessingException {
return new Tool.Builder()
.name("search-fhir-resources")
.description("Search an existing FHIR resources")
.inputSchema(mapper.readValue(SEARCH_FHIR_RESOURCES_SCHEMA, McpSchema.JsonSchema.class))
.build();
}
public static Tool createFhirTransaction() throws JsonProcessingException {
return new Tool.Builder()
.name("create-fhir-transaction")
.description("Create a FHIR transaction")
.inputSchema(mapper.readValue(CREATE_FHIR_TRANSACTION_SCHEMA, McpSchema.JsonSchema.class))
.build();
}
public static Tool callCdsHook() throws JsonProcessingException {
return new Tool.Builder()
.name("call-cds-hook")
.description("Call a CDS Hook")
.inputSchema(mapper.readValue(CALL_CDS_HOOK_SCHEMA_2_0_1, McpSchema.JsonSchema.class))
.build();
}
public static final ObjectMapper mapper = new ObjectMapper()
.enable(JsonParser.Feature.ALLOW_COMMENTS)
.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES)
.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
.enable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}

View File

@@ -9,8 +9,8 @@ import ca.uhn.fhir.mdm.rules.config.MdmRuleValidator;
import ca.uhn.fhir.mdm.rules.config.MdmSettings;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.DefaultResourceLoader;
@@ -20,7 +20,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Configuration
@Conditional(MdmConfigCondition.class)
@ConditionalOnProperty(prefix = "hapi.fhir", name = "mdm_enabled")
@Import({MdmConsumerConfig.class, MdmSubmitterConfig.class, NicknameServiceConfig.class})
public class MdmConfig {

View File

@@ -1,13 +0,0 @@
package ca.uhn.fhir.jpa.starter.mdm;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class MdmConfigCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) {
String property = conditionContext.getEnvironment().getProperty("hapi.fhir.mdm_enabled");
return Boolean.parseBoolean(property);
}
}

View File

@@ -1,22 +1,7 @@
package ca.uhn.fhir.jpa.starter.util;
import ca.uhn.fhir.jpa.config.HapiFhirLocalContainerEntityManagerFactoryBean;
import ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers;
import ca.uhn.fhir.jpa.search.elastic.ElasticsearchHibernatePropertiesBuilder;
import org.apache.lucene.util.Version;
import org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings;
import org.hibernate.search.backend.elasticsearch.index.IndexStatus;
import org.hibernate.search.backend.lucene.cfg.LuceneBackendSettings;
import org.hibernate.search.backend.lucene.cfg.LuceneIndexSettings;
import org.hibernate.search.backend.lucene.lowlevel.directory.impl.LocalFileSystemDirectoryProvider;
import org.hibernate.search.engine.cfg.BackendSettings;
import org.hibernate.search.mapper.orm.automaticindexing.session.AutomaticIndexingSynchronizationStrategyNames;
import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
import org.hibernate.search.mapper.orm.schema.management.SchemaManagementStrategyName;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
@@ -25,142 +10,11 @@ import org.springframework.core.env.PropertySource;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import static java.util.Objects.requireNonNullElse;
public class EnvironmentHelper {
public static Properties getHibernateProperties(
ConfigurableEnvironment environment, ConfigurableListableBeanFactory myConfigurableListableBeanFactory) {
Properties properties = new Properties();
Map<String, Object> jpaProps = getPropertiesStartingWith(environment, "spring.jpa.properties");
for (Map.Entry<String, Object> entry : jpaProps.entrySet()) {
String strippedKey = entry.getKey().replace("spring.jpa.properties.", "");
properties.put(strippedKey, entry.getValue().toString());
}
// also check for JPA properties set as environment variables, this is slightly hacky and doesn't cover all
// the naming conventions Springboot allows
// but there doesn't seem to be a better/deterministic way to get these properties when they are set as ENV
// variables and this at least provides
// a way to set them (in a docker container, for instance)
Map<String, Object> jpaPropsEnv = getPropertiesStartingWith(environment, "SPRING_JPA_PROPERTIES");
for (Map.Entry<String, Object> entry : jpaPropsEnv.entrySet()) {
String strippedKey = entry.getKey().replace("SPRING_JPA_PROPERTIES_", "");
strippedKey = strippedKey.replaceAll("_", ".");
strippedKey = strippedKey.toLowerCase();
properties.put(strippedKey, entry.getValue().toString());
}
// Spring Boot Autoconfiguration defaults
properties.putIfAbsent(AvailableSettings.SCANNER, "org.hibernate.boot.archive.scan.internal.DisabledScanner");
properties.putIfAbsent(
AvailableSettings.IMPLICIT_NAMING_STRATEGY, SpringImplicitNamingStrategy.class.getName());
properties.putIfAbsent(
AvailableSettings.PHYSICAL_NAMING_STRATEGY, CamelCaseToUnderscoresNamingStrategy.class.getName());
// TODO The bean factory should be added as parameter but that requires that it can be injected from the
// entityManagerFactory bean from xBaseConfig
// properties.putIfAbsent(AvailableSettings.BEAN_CONTAINER, new SpringBeanContainer(beanFactory));
// hapi-fhir-jpaserver-base "sensible defaults"
Map<String, Object> hapiJpaPropertyMap = new HapiFhirLocalContainerEntityManagerFactoryBean(
myConfigurableListableBeanFactory)
.getJpaPropertyMap();
hapiJpaPropertyMap.forEach(properties::putIfAbsent);
// hapi-fhir-jpaserver-starter defaults
properties.putIfAbsent(AvailableSettings.FORMAT_SQL, false);
properties.putIfAbsent(AvailableSettings.SHOW_SQL, false);
properties.putIfAbsent(AvailableSettings.HBM2DDL_AUTO, "update");
properties.putIfAbsent(AvailableSettings.STATEMENT_BATCH_SIZE, 20);
properties.putIfAbsent(AvailableSettings.USE_QUERY_CACHE, false);
properties.putIfAbsent(AvailableSettings.USE_SECOND_LEVEL_CACHE, false);
properties.putIfAbsent(AvailableSettings.USE_STRUCTURED_CACHE, false);
properties.putIfAbsent(AvailableSettings.USE_MINIMAL_PUTS, false);
// Hibernate Search defaults
properties.putIfAbsent(HibernateOrmMapperSettings.ENABLED, false);
if (Boolean.parseBoolean(String.valueOf(properties.get(HibernateOrmMapperSettings.ENABLED)))) {
if (isElasticsearchEnabled(environment)) {
properties.putIfAbsent(
BackendSettings.backendKey(BackendSettings.TYPE), ElasticsearchBackendSettings.TYPE_NAME);
} else {
properties.putIfAbsent(
BackendSettings.backendKey(BackendSettings.TYPE), LuceneBackendSettings.TYPE_NAME);
}
if (properties
.get(BackendSettings.backendKey(BackendSettings.TYPE))
.equals(LuceneBackendSettings.TYPE_NAME)) {
properties.putIfAbsent(
BackendSettings.backendKey(LuceneIndexSettings.DIRECTORY_TYPE),
LocalFileSystemDirectoryProvider.NAME);
properties.putIfAbsent(
BackendSettings.backendKey(LuceneIndexSettings.DIRECTORY_ROOT), "target/lucenefiles");
properties.putIfAbsent(
BackendSettings.backendKey(LuceneBackendSettings.ANALYSIS_CONFIGURER),
HapiHSearchAnalysisConfigurers.HapiLuceneAnalysisConfigurer.class.getName());
properties.putIfAbsent(
BackendSettings.backendKey(LuceneBackendSettings.LUCENE_VERSION), Version.LATEST);
} else if (properties
.get(BackendSettings.backendKey(BackendSettings.TYPE))
.equals(ElasticsearchBackendSettings.TYPE_NAME)) {
ElasticsearchHibernatePropertiesBuilder builder = new ElasticsearchHibernatePropertiesBuilder();
IndexStatus requiredIndexStatus =
environment.getProperty("elasticsearch.required_index_status", IndexStatus.class);
builder.setRequiredIndexStatus(requireNonNullElse(requiredIndexStatus, IndexStatus.YELLOW));
builder.setHosts(getElasticsearchServerUrl(environment));
builder.setUsername(getElasticsearchServerUsername(environment));
builder.setPassword(getElasticsearchServerPassword(environment));
builder.setProtocol(getElasticsearchServerProtocol(environment));
SchemaManagementStrategyName indexSchemaManagementStrategy = environment.getProperty(
"elasticsearch.schema_management_strategy", SchemaManagementStrategyName.class);
builder.setIndexSchemaManagementStrategy(
requireNonNullElse(indexSchemaManagementStrategy, SchemaManagementStrategyName.CREATE));
Boolean refreshAfterWrite =
environment.getProperty("elasticsearch.debug.refresh_after_write", Boolean.class);
if (refreshAfterWrite == null || !refreshAfterWrite) {
builder.setDebugIndexSyncStrategy(AutomaticIndexingSynchronizationStrategyNames.ASYNC);
} else {
builder.setDebugIndexSyncStrategy(AutomaticIndexingSynchronizationStrategyNames.READ_SYNC);
}
builder.setDebugPrettyPrintJsonLog(requireNonNullElse(
environment.getProperty("elasticsearch.debug.pretty_print_json_log", Boolean.class), false));
builder.apply(properties);
} else {
throw new UnsupportedOperationException("Unsupported Hibernate Search backend: "
+ properties.get(BackendSettings.backendKey(BackendSettings.TYPE)));
}
}
return properties;
}
public static String getElasticsearchServerUrl(ConfigurableEnvironment environment) {
return environment.getProperty("elasticsearch.rest_url", String.class);
}
public static String getElasticsearchServerProtocol(ConfigurableEnvironment environment) {
return environment.getProperty("elasticsearch.protocol", String.class, "http");
}
public static String getElasticsearchServerUsername(ConfigurableEnvironment environment) {
return environment.getProperty("elasticsearch.username");
}
public static String getElasticsearchServerPassword(ConfigurableEnvironment environment) {
return environment.getProperty("elasticsearch.password");
}
public static Boolean isElasticsearchEnabled(ConfigurableEnvironment environment) {
if (environment.getProperty("elasticsearch.enabled", Boolean.class) != null) {
return environment.getProperty("elasticsearch.enabled", Boolean.class);
} else {
return false;
}
public static <T> T getConfiguration(ConditionContext context, String path, Class<T> clazz) {
return Binder.get(context.getEnvironment()).bind(path, clazz).orElse(null);
}
public static Map<String, Object> getPropertiesStartingWith(ConfigurableEnvironment aEnv, String aKeyPrefix) {

View File

@@ -0,0 +1,9 @@
package ca.uhn.fhir.rest.server;
import io.modelcontextprotocol.server.McpServerFeatures;
import java.util.List;
public interface McpBridge {
List<McpServerFeatures.SyncToolSpecification> generateTools();
}

View File

@@ -0,0 +1,117 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.starter.cdshooks.CdsHooksRequest;
import ca.uhn.fhir.jpa.starter.mcp.Interaction;
import ca.uhn.fhir.jpa.starter.mcp.ToolFactory;
import ca.uhn.fhir.rest.api.server.cdshooks.CdsServiceRequestContextJson;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceRegistry;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Component
public class McpCdsBridge implements McpBridge {
private static final Logger logger = LoggerFactory.getLogger(McpCdsBridge.class);
private final ICdsServiceRegistry cdsServiceRegistry;
private final ObjectMapper objectMapper;
private final FhirContext fhirContext;
public McpCdsBridge(FhirContext fhirContext, ICdsServiceRegistry cdsServiceRegistry, ObjectMapper objectMapper) {
this.fhirContext = fhirContext;
this.cdsServiceRegistry = cdsServiceRegistry;
this.objectMapper = objectMapper;
}
public List<McpServerFeatures.SyncToolSpecification> generateTools() {
try {
return List.of(new McpServerFeatures.SyncToolSpecification.Builder()
.tool(ToolFactory.callCdsHook())
.callHandler((exchange, request) -> getToolResult(request, Interaction.CALL_CDS_HOOK))
.build());
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
private McpSchema.CallToolResult getToolResult(McpSchema.CallToolRequest contextMap, Interaction interaction) {
if (interaction != Interaction.CALL_CDS_HOOK)
throw new UnsupportedOperationException("Unsupported interaction: " + interaction);
var cdsInvocation = constructCdsHooksRequest(contextMap);
var serviceResponseJson = cdsServiceRegistry.callService(
contextMap.arguments().get("service").toString(), cdsInvocation);
final String content;
try {
content = objectMapper.writeValueAsString(serviceResponseJson);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
return McpSchema.CallToolResult.builder()
.addContent(new McpSchema.TextContent(content))
.build();
}
private @NotNull CdsHooksRequest constructCdsHooksRequest(McpSchema.CallToolRequest callToolRequest) {
// TODO Build up CDS Hooks request JSON from contextMap
var contextMap = callToolRequest.arguments();
var request = new CdsHooksRequest();
request.setHook(contextMap.get("hook").toString());
request.setHookInstance(contextMap.get("hookInstance").toString());
// Context
var context = new CdsServiceRequestContextJson();
Object hookContextObj = contextMap.get("hookContext");
if (hookContextObj instanceof Map<?, ?> hookContext) {
if (hookContext.containsKey("userId")) {
context.put("userId", String.valueOf(hookContext.get("userId")));
}
if (hookContext.containsKey("patientId")) {
context.put("patientId", String.valueOf(hookContext.get("patientId")));
}
if (hookContext.containsKey("encounterId")) {
context.put("encounterId", String.valueOf(hookContext.get("encounterId")));
}
}
request.setContext(context);
// Prefetch
if (contextMap.containsKey("prefetch")) {
var prefetch = contextMap.get("prefetch");
if (prefetch instanceof Map) {
@SuppressWarnings("unchecked")
var prefetchMap = (Map<String, Object>) prefetch;
for (Map.Entry<String, Object> entry : prefetchMap.entrySet()) {
var key = entry.getKey();
var value = entry.getValue();
// Object is a String -> Object map
// Use a standard JSON library to convert it
var resource = fhirContext.newJsonParser().parseResource(new Gson().toJson(value));
request.addPrefetch(key, resource);
}
} else {
logger.warn(
"Prefetch object is not a Map: {}",
prefetch == null ? "null" : prefetch.getClass().getName());
}
}
return request;
}
}

View File

@@ -0,0 +1,101 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.starter.mcp.CallToolResultFactory;
import ca.uhn.fhir.jpa.starter.mcp.Interaction;
import ca.uhn.fhir.jpa.starter.mcp.RequestBuilder;
import ca.uhn.fhir.jpa.starter.mcp.ToolFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class McpFhirBridge implements McpBridge {
private static final Logger logger = LoggerFactory.getLogger(McpFhirBridge.class);
private final RestfulServer restfulServer;
private final FhirContext fhirContext;
public McpFhirBridge(RestfulServer restfulServer) {
this.restfulServer = restfulServer;
this.fhirContext = restfulServer.getFhirContext();
}
public List<McpServerFeatures.SyncToolSpecification> generateTools() {
try {
return List.of(
new McpServerFeatures.SyncToolSpecification.Builder()
.tool(ToolFactory.createFhirResource())
.callHandler((exchange, request) -> getToolResult(request, Interaction.CREATE))
.build(),
new McpServerFeatures.SyncToolSpecification.Builder()
.tool(ToolFactory.readFhirResource())
.callHandler((exchange, request) -> getToolResult(request, Interaction.READ))
.build(),
new McpServerFeatures.SyncToolSpecification.Builder()
.tool(ToolFactory.updateFhirResource())
.callHandler((exchange, request) -> getToolResult(request, Interaction.UPDATE))
.build(),
new McpServerFeatures.SyncToolSpecification.Builder()
.tool(ToolFactory.deleteFhirResource())
.callHandler((exchange, request) -> getToolResult(request, Interaction.DELETE))
.build(),
new McpServerFeatures.SyncToolSpecification.Builder()
.tool(ToolFactory.conditionalPatchFhirResource())
.callHandler((exchange, request) -> getToolResult(request, Interaction.PATCH))
.build(),
new McpServerFeatures.SyncToolSpecification.Builder()
.tool(ToolFactory.searchFhirResources())
.callHandler((exchange, request) -> getToolResult(request, Interaction.SEARCH))
.build(),
new McpServerFeatures.SyncToolSpecification.Builder()
.tool(ToolFactory.conditionalUpdateFhirResource())
.callHandler((exchange, request) -> getToolResult(request, Interaction.UPDATE))
.build(),
new McpServerFeatures.SyncToolSpecification.Builder()
.tool(ToolFactory.patchFhirResource())
.callHandler((exchange, request) -> getToolResult(request, Interaction.PATCH))
.build(),
new McpServerFeatures.SyncToolSpecification.Builder()
.tool(ToolFactory.createFhirTransaction())
.callHandler((exchange, request) -> getToolResult(request, Interaction.TRANSACTION))
.build());
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
private McpSchema.CallToolResult getToolResult(McpSchema.CallToolRequest contextMap, Interaction interaction) {
var response = new MockHttpServletResponse();
var request = new RequestBuilder(fhirContext, contextMap.arguments(), interaction).buildRequest();
try {
restfulServer.handleRequest(interaction.asRequestType(), request, response);
var status = response.getStatus();
var body = response.getContentAsString();
if (status >= 200 && status < 300) {
if (body.isBlank()) {
return CallToolResultFactory.failure("Empty successful response for " + interaction);
}
return CallToolResultFactory.success(
contextMap.arguments().get("resourceType").toString(), interaction, body, status);
} else {
return CallToolResultFactory.failure(String.format("FHIR server error %d: %s", status, body));
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
return CallToolResultFactory.failure("Unexpected error: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,427 @@
# -------------------------------------------------------------------------------------
# Server & Spring Boot
# -------------------------------------------------------------------------------------
server:
# Uncomment to serve FHIR under a non-default context path (e.g., /example/path/fhir)
# servlet:
# context-path: /example/path
port: 8080
tomcat:
# allow | as a separator in URLs
relaxed-query-chars: "|"
management:
# Actuator endpoints: only /actuator/health exposed by default
endpoints:
enabled-by-default: false
web:
exposure:
include: "health" # or "info,health,prometheus,metrics" or "*" for all
endpoint:
info:
enabled: true
metrics:
enabled: true
health:
enabled: true
probes:
enabled: true
group:
liveness:
include: [ "livenessState", "readinessState" ]
prometheus:
enabled: true
prometheus:
metrics:
export:
enabled: true
spring:
# -------------------------------------------------------------------------------
# A. Spring AI — Model Context Protocol (MCP)
# -------------------------------------------------------------------------------
ai:
# Run e.g. `npx @modelcontextprotocol/inspector` and connect to http://localhost:8080/mcp/messages using Streamable HTTP
# Add the following to the MCP server settings file in e.g. cursor or claude (Desktop applications) for local debugging:
# cursor:
# {
# "mcpServers": {
# "hapi": {
# "url": "http://localhost:8080/mcp/messages"
# }
# }
# }
# or claude:
# {
# "mcpServers": {
# "hapi": {
# "command": "npx",
# "args": [
# "mcp-remote@latest",
# "http://localhost:8080/mcp/messages"
# ]
# }
# }
# }
mcp:
server:
name: FHIR MCP Server
version: 1.0.0
instructions: "This server provides access to a FHIR RESTful API. You can use it to query FHIR resources, perform operations, and retrieve data in a structured format."
enabled: true
streamable-http:
mcp-endpoint: /mcp/messages
# -------------------------------------------------------------------------------
# B. Core Spring
# -------------------------------------------------------------------------------
main:
allow-bean-definition-overriding: false
allow-circular-references: true
autoconfigure:
# This exclude is only needed for setups not using Elasticsearch where the elasticsearch sniff is not needed.
exclude: org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration
flyway:
enabled: false
baseline-on-migrate: true
fail-on-missing-locations: false
datasource:
# url: "jdbc:h2:file:./target/database/h2"
url: jdbc:h2:mem:test_mem
username: sa
password: null
driver-class-name: org.h2.Driver
# max-active: 15 # (ignored with HikariCP; use hikari.maximum-pool-size)
hikari:
maximum-pool-size: 10
jpa:
properties:
hibernate:
format_sql: false
show_sql: false
# Hibernate dialect is auto-detected except for H2/Postgres.
# If using H2: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
# If using Postgres: ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect
dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
# --- Optional Hibernate DDL & tuning (commented out from source) ---
hbm2ddl:
auto: update
jdbc:
batch_size: 20
cache:
use_query_cache: false
use_second_level_cache: false
use_structured_entries: false
use_minimal_puts: false
# --- Hibernate Search (Lucene/Elasticsearch) ---
search:
enabled: false
# Lucene backend (default example)
# backend:
# type: lucene
# analysis:
# configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiLuceneAnalysisConfigurer
# directory:
# type: local-filesystem
# root: target/lucenefiles
# lucene_version: lucene_current
# Elasticsearch backend (alternative) — see also hapi.fhir.elasticsearch section in docs
# backend:
# type: elasticsearch
# analysis:
# configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer
# -------------------------------------------------------------------------------------
# HAPI FHIR — grouped by domain
# -------------------------------------------------------------------------------------
hapi:
fhir:
# -------------------------------------------------------------------------------
# A. Core Server & API
# -------------------------------------------------------------------------------
openapi_enabled: true # Swagger UI at /fhir/swagger-ui/index.html; API docs at /fhir/api-docs
fhir_version: R4 # DSTU2 | DSTU3 | R4 | R5
# use_apache_address_strategy: false
# use_apache_address_strategy_https: false
# custom_content_path: ./custom # folder name must be 'custom'
# app_content_path: ./configs/app # served under /web/app
# server_address: http://hapi.fhir.org/baseR4
# defer_indexing_for_codesystems_of_size: 101
# -------------------------------------------------------------------------------
# B. Implementation Guides (IG) & Package Install
# -------------------------------------------------------------------------------
ig_runtime_upload_enabled: false
# validate_resource_status_for_package_upload: false # default true
# install_transitive_ig_dependencies: true
implementationguides:
cql:
name: hl7.fhir.uv.cql
version: 2.0.0
installMode: STORE_AND_INSTALL
installResourceTypes: [ "CodeSystem", "ValueSet", "ConceptMap", "ActivityDefinition", "PlanDefinition", "Library", "Measure", "StructureDefinition" ]
# swiss:
# name: swiss.mednet.fhir
# version: 0.8.0
# reloadExisting: false
# installMode: STORE_AND_INSTALL
# ips_1_0_0:
# packageUrl: https://costateixeira.github.io/smart-ips-pilgrimage-fulltest/package.tgz
# name: smart.who.int.ips-pilgrimage-test
# version: 0.1.0
# installMode: STORE_AND_INSTALL
# additionalResourceFolders:
# - example
# - example2
# supported_resource_types:
# - Patient
# - Observation
# allowed_bundle_types: COLLECTION,DOCUMENT,MESSAGE,TRANSACTION,TRANSACTIONRESPONSE,BATCH,BATCHRESPONSE,HISTORY,SEARCHSET
# -------------------------------------------------------------------------------
# C. Clinical Reasoning / CQL / Care Gaps / CDS Hooks
# -------------------------------------------------------------------------------
cr:
enabled: true # exposes Clinical Reasoning operation endpoints
caregaps:
reporter: "default"
section_author: "default"
terminologyServerClientSettings:
maxRetryCount: 3
retryIntervalMillis: 1000
timeoutSeconds: 30
socketTimeout: 60
cql:
use_embedded_libraries: true
compiler:
# low-level compiler options (typically not needed)
# validate_units: true
# verify_only: false
# compatibility_level: "1.5"
error_level: Info
signature_level: All
# analyze_data_requirements: false
# collapse_data_requirements: false
# translator_format: JSON
# enable_date_range_optimization: true
enable_annotations: true
enable_locators: true
enable_results_type: true
enable_detailed_errors: true
# disable_list_traversal: false
# disable_list_demotion: false
# enable_interval_demotion: false
# enable_interval_promotion: false
# disable_method_invocation: false
# require_from_keyword: false
# disable_default_model_info_load: false
runtime:
debug_logging_enabled: false
# enable_validation: false
# enable_expression_caching: true
terminology:
valueset_preexpansion_mode: REQUIRE # USE_IF_PRESENT | REQUIRE | IGNORE
valueset_expansion_mode: PERFORM_NAIVE_EXPANSION # AUTO | USE_EXPANSION_OPERATION | PERFORM_NAIVE_EXPANSION
valueset_membership_mode: USE_EXPANSION # AUTO | USE_VALIDATE_CODE_OPERATION | USE_EXPANSION
code_lookup_mode: USE_CODESYSTEM_URL # AUTO | USE_VALIDATE_CODE_OPERATION | USE_CODESYSTEM_URL
data:
search_parameter_mode: USE_SEARCH_PARAMETERS # AUTO | USE_SEARCH_PARAMETERS | FILTER_IN_MEMORY
terminology_parameter_mode: FILTER_IN_MEMORY # AUTO | USE_VALUE_SET_URL | USE_INLINE_CODES | FILTER_IN_MEMORY
profile_mode: DECLARED # ENFORCED | DECLARED | OPTIONAL | TRUST | OFF
cdshooks:
enabled: true
clientIdHeaderName: client_id
# -------------------------------------------------------------------------------
# D. Search & Indexing
# -------------------------------------------------------------------------------
# NOTE: Extended Lucene/Elasticsearch indexing is experimental.
# See https://hapifhir.io/hapi-fhir/docs/server_jpa/elastic.html
advanced_lucene_indexing: false
search_index_full_text_enabled: false
# language_search_parameter_enabled: true
# upliftedRefchains_enabled: true
# index_storage_optimized: false
# enable_index_missing_fields: false
# enable_index_of_type: true
# enable_index_contained_resource: false
# store_resource_in_lucene_index_enabled: true
# -------------------------------------------------------------------------------
# E. Bulk Operations
# -------------------------------------------------------------------------------
bulk_export_enabled: false
bulk_import_enabled: false
# -------------------------------------------------------------------------------
# F. Write / Delete / Integrity
# -------------------------------------------------------------------------------
# allow_cascading_deletes: true
# allow_contains_searches: true
allow_external_references: true
# allow_multiple_delete: true
# allow_override_default_search_params: true
# auto_create_placeholder_reference_targets: false
# mass_ingestion_mode_enabled: false
# auto_version_reference_at_paths: Device.patient, Device.location, Device.parent, DeviceMetric.parent, DeviceMetric.source, Observation.device, Observation.subject
# client_id_strategy: ALPHANUMERIC
# server_id_strategy: SEQUENTIAL_NUMERIC
# enforce_referential_integrity_on_delete: false
# enforce_referential_integrity_on_write: false
# etag_support_enabled: true
# expunge_enabled: true
# fhirpath_interceptor_enabled: false
# filter_search_enabled: true
# graphql_enabled: true
# -------------------------------------------------------------------------------
# G. Narrative & Validation
# -------------------------------------------------------------------------------
narrative_enabled: false
# validation:
# requests_enabled: true
# responses_enabled: true
# -------------------------------------------------------------------------------
# H. MDM (Master Data Management)
# -------------------------------------------------------------------------------
mdm_enabled: false
mdm_rules_json_location: "mdm-rules.json"
# userRequestRetryVersionConflictsInterceptorEnabled: false
# -------------------------------------------------------------------------------
# I. Terminology / ValueSet Expansion
# -------------------------------------------------------------------------------
# pre_expand_value_sets: true
# enable_task_pre_expand_value_sets: true
# pre_expand_value_sets_default_count: 1000
# pre_expand_value_sets_max_count: 1000
# maximum_expansion_size: 1000
logical_urls:
- http://terminology.hl7.org/*
- https://terminology.hl7.org/*
- http://snomed.info/*
- https://snomed.info/*
- http://unitsofmeasure.org/*
- https://unitsofmeasure.org/*
- http://loinc.org/*
- https://loinc.org/*
# -------------------------------------------------------------------------------
# J. Partitioning & Multitenancy
# -------------------------------------------------------------------------------
# partitioning:
# allow_references_across_partitions: false
# partitioning_include_in_search_hashes: false
# default_partition_id: 0
# database_partition_mode_enabled: true
# patient_id_partitioning_mode: true
# request_tenant_partitioning_mode: false
# -------------------------------------------------------------------------------
# K. CORS
# -------------------------------------------------------------------------------
cors:
allow_Credentials: true
allowed_origin:
- "*"
# -------------------------------------------------------------------------------
# L. Search Orchestration
# -------------------------------------------------------------------------------
search-coord-core-pool-size: 20
search-coord-max-pool-size: 100
search-coord-queue-capacity: 200
# Search Prefetch Thresholds.
# This setting sets the number of search results to prefetch. For example, if this list
# is set to [100, 1000, -1] then the server will initially load 100 results and not
# attempt to load more. If the user requests subsequent page(s) of results and goes
# past 100 results, the system will load the next 900 (up to the following threshold of 1000).
# The system will progressively work through these thresholds.
# A threshold of -1 means to load all results. Note that if the final threshold is a
# number other than -1, the system will never prefetch more than the given number.
# CSV list; -1 as final value means "all"
search_prefetch_thresholds: 13,503,2003,-1
# -------------------------------------------------------------------------------
# M. Extensibility (custom beans / interceptors / providers)
# -------------------------------------------------------------------------------
# comma-separated package names, will be @ComponentScan'ed by Spring to allow for creating custom Spring beans
# custom-provider-classes:
# custom-interceptor-classes:
# custom-provider-classes:
# store_meta_source_information: NONE
# bundle_batch_pool_size: 10
# bundle_batch_pool_max_size: 50
# -------------------------------------------------------------------------------
# N. Logging
# -------------------------------------------------------------------------------
# logger:
# error_format: "ERROR - ${requestVerb} ${requestUrl}"
# format: >-
# Path[${servletPath}] Source[${requestHeader.x-forwarded-for}]
# Operation[${operationType} ${operationName} ${idOrResourceName}]
# UA[${requestHeader.user-agent}] Params[${requestParameters}]
# ResponseEncoding[${responseEncodingNoDefault}]
# log_exceptions: true
# name: fhirtest.access
# -------------------------------------------------------------------------------
# O. Storage / Pagination / Caching
# -------------------------------------------------------------------------------
# max_binary_size: 104857600
# max_page_size: 200
# retain_cached_searches_mins: 60
# reuse_cached_search_results_millis: 60000
inline_resource_storage_below_size: 4000
# -------------------------------------------------------------------------------
# P. Remote Terminology Service (disabled by default)
# -------------------------------------------------------------------------------
# remote_terminology_service:
# all:
# system: "*"
# url: "https://tx.fhir.org/r4/"
# snomed:
# system: "http://snomed.info/sct"
# url: "https://tx.fhir.org/r4/"
# loinc:
# system: "http://loinc.org"
# url: "https://hapi.fhir.org/baseR4/"
# -------------------------------------------------------------------------------
# Q. Subscriptions (disabled by default)
# -------------------------------------------------------------------------------
# subscription:
# resthook_enabled: true
# websocket_enabled: false
# polling_interval_ms: 5000
# immediately_queued: false
# email:
# from: some@test.com
# host: google.com
# port:
# username:
# password:
# auth:
# startTlsEnable:
# startTlsRequired:
# quitWait:
# -------------------------------------------------------------------------------
# R. LastN (analytics)
# -------------------------------------------------------------------------------
# lastn_enabled: true

View File

@@ -1,20 +1,22 @@
#Uncomment the "servlet" and "context-path" lines below to make the fhir endpoint available at /example/path/fhir instead of the default value of /fhir
# -------------------------------------------------------------------------------------
# Server & Spring Boot
# -------------------------------------------------------------------------------------
server:
# Uncomment to serve FHIR under a non-default context path (e.g., /example/path/fhir)
# servlet:
# context-path: /example/path
port: 8080
tomcat:
# allow | as a separator in the URL
# allow | as a separator in URLs
relaxed-query-chars: "|"
#Adds the option to go to e.g. http://localhost:8080/actuator/health for seeing the running configuration
#see https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints
management:
#The following configuration will enable the actuator endpoints at /actuator/health, /actuator/info, /actuator/prometheus, /actuator/metrics. For security purposes, only /actuator/health is enabled by default.
# Actuator endpoints: only /actuator/health exposed by default
endpoints:
enabled-by-default: false
web:
exposure:
include: 'health' # or e.g. 'info,health,prometheus,metrics' or '*' for all
include: "health" # or "info,health,prometheus,metrics" or "*" for all
endpoint:
info:
enabled: true
@@ -26,66 +28,166 @@ management:
enabled: true
group:
liveness:
include:
- livenessState
- readinessState
include: [ "livenessState", "readinessState" ]
prometheus:
enabled: true
prometheus:
metrics:
export:
enabled: true
spring:
# -------------------------------------------------------------------------------
# A. Spring AI — Model Context Protocol (MCP)
# -------------------------------------------------------------------------------
ai:
# Run e.g. `npx @modelcontextprotocol/inspector` and connect to http://localhost:8080/mcp/messages using Streamable HTTP
# Add the following to the MCP server settings file in e.g. cursor or claude (Desktop applications) for local debugging:
# cursor:
# {
# "mcpServers": {
# "hapi": {
# "url": "http://localhost:8080/mcp/messages"
# }
# }
# }
# or claude:
# {
# "mcpServers": {
# "hapi": {
# "command": "npx",
# "args": [
# "mcp-remote@latest",
# "http://localhost:8080/mcp/messages"
# ]
# }
# }
# }
mcp:
server:
name: FHIR MCP Server
version: 1.0.0
instructions: "This server provides access to a FHIR RESTful API. You can use it to query FHIR resources, perform operations, and retrieve data in a structured format."
enabled: true
streamable-http:
mcp-endpoint: /mcp/messages
# -------------------------------------------------------------------------------
# B. Core Spring
# -------------------------------------------------------------------------------
main:
allow-bean-definition-overriding: false
allow-circular-references: true
autoconfigure:
# This exclude is only needed for setups not using Elasticsearch where the elasticsearch sniff is not needed.
exclude: org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration
flyway:
enabled: false
baselineOnMigrate: true
baseline-on-migrate: true
fail-on-missing-locations: false
datasource:
#url: 'jdbc:h2:file:./target/database/h2'
# url: "jdbc:h2:file:./target/database/h2"
url: jdbc:h2:mem:test_mem
username: sa
password: null
driverClassName: org.h2.Driver
max-active: 15
# database connection pool size
driver-class-name: org.h2.Driver
# max-active: 15 # (ignored with HikariCP; use hikari.maximum-pool-size)
hikari:
maximum-pool-size: 10
jpa:
properties:
hibernate.format_sql: false
hibernate.show_sql: false
hibernate:
format_sql: false
show_sql: false
# Hibernate dialect is auto-detected except for H2/Postgres.
# If using H2: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
# If using Postgres: ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect
dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
#Hibernate dialect is automatically detected except Postgres and H2.
#If using H2, then supply the value of ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
#If using postgres, then supply the value of ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect
hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
# hibernate.hbm2ddl.auto: update
# hibernate.jdbc.batch_size: 20
# hibernate.cache.use_query_cache: false
# hibernate.cache.use_second_level_cache: false
# hibernate.cache.use_structured_entries: false
# hibernate.cache.use_minimal_puts: false
# --- Optional Hibernate DDL & tuning (commented out from source) ---
hbm2ddl:
auto: update
jdbc:
batch_size: 20
cache:
use_query_cache: false
use_second_level_cache: false
use_structured_entries: false
use_minimal_puts: false
### These settings will enable fulltext search with lucene or elastic
hibernate.search.enabled: false
### lucene parameters
# hibernate.search.backend.type: lucene
# hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiLuceneAnalysisConfigurer
# hibernate.search.backend.directory.type: local-filesystem
# hibernate.search.backend.directory.root: target/lucenefiles
# hibernate.search.backend.lucene_version: lucene_current
### elastic parameters ===> see also elasticsearch section below <===
# hibernate.search.backend.type: elasticsearch
# hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer
# --- Hibernate Search (Lucene/Elasticsearch) ---
search:
enabled: false
# Lucene backend (default example)
# backend:
# type: lucene
# analysis:
# configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiLuceneAnalysisConfigurer
# directory:
# type: local-filesystem
# root: target/lucenefiles
# lucene_version: lucene_current
# Elasticsearch backend (alternative) — see also hapi.fhir.elasticsearch section in docs
# backend:
# type: elasticsearch
# analysis:
# configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer
# -------------------------------------------------------------------------------------
# HAPI FHIR — grouped by domain
# -------------------------------------------------------------------------------------
hapi:
fhir:
### This flag when enabled to true, will avail evaluate measure operations from CR Module.
### Flag is false by default, can be passed as command line argument to override.
# -------------------------------------------------------------------------------
# A. Core Server & API
# -------------------------------------------------------------------------------
openapi_enabled: true # Swagger UI at /fhir/swagger-ui/index.html; API docs at /fhir/api-docs
fhir_version: R4 # DSTU2 | DSTU3 | R4 | R5
# use_apache_address_strategy: false
# use_apache_address_strategy_https: false
# custom_content_path: ./custom # folder name must be 'custom'
# app_content_path: ./configs/app # served under /web/app
# server_address: http://hapi.fhir.org/baseR4
# defer_indexing_for_codesystems_of_size: 101
# -------------------------------------------------------------------------------
# B. Implementation Guides (IG) & Package Install
# -------------------------------------------------------------------------------
ig_runtime_upload_enabled: false
# validate_resource_status_for_package_upload: false # default true
# install_transitive_ig_dependencies: true
# implementationguides:
# swiss:
# name: swiss.mednet.fhir
# version: 0.8.0
# reloadExisting: false
# installMode: STORE_AND_INSTALL
# ips_1_0_0:
# packageUrl: https://costateixeira.github.io/smart-ips-pilgrimage-fulltest/package.tgz
# name: smart.who.int.ips-pilgrimage-test
# version: 0.1.0
# installMode: STORE_AND_INSTALL
# additionalResourceFolders:
# - example
# - example2
# supported_resource_types:
# - Patient
# - Observation
# allowed_bundle_types: COLLECTION,DOCUMENT,MESSAGE,TRANSACTION,TRANSACTIONRESPONSE,BATCH,BATCHRESPONSE,HISTORY,SEARCHSET
# -------------------------------------------------------------------------------
# C. Clinical Reasoning / CQL / Care Gaps / CDS Hooks
# -------------------------------------------------------------------------------
cr:
enabled: false
enabled: false # exposes Clinical Reasoning operation endpoints
caregaps:
reporter: "default"
section_author: "default"
@@ -97,8 +199,7 @@ hapi:
cql:
use_embedded_libraries: true
compiler:
### These are low-level compiler options.
### They are not typically needed by most users.
# low-level compiler options (typically not needed)
# validate_units: true
# verify_only: false
# compatibility_level: "1.5"
@@ -124,64 +225,42 @@ hapi:
# enable_validation: false
# enable_expression_caching: true
terminology:
valueset_preexpansion_mode: REQUIRE # USE_IF_PRESENT, REQUIRE, IGNORE
valueset_expansion_mode: PERFORM_NAIVE_EXPANSION # AUTO, USE_EXPANSION_OPERATION, PERFORM_NAIVE_EXPANSION
valueset_membership_mode: USE_EXPANSION # AUTO, USE_VALIDATE_CODE_OPERATION, USE_EXPANSION
code_lookup_mode: USE_VALIDATE_CODE_OPERATION # AUTO, USE_VALIDATE_CODE_OPERATION, USE_CODESYSTEM_URL
valueset_preexpansion_mode: REQUIRE # USE_IF_PRESENT | REQUIRE | IGNORE
valueset_expansion_mode: PERFORM_NAIVE_EXPANSION # AUTO | USE_EXPANSION_OPERATION | PERFORM_NAIVE_EXPANSION
valueset_membership_mode: USE_EXPANSION # AUTO | USE_VALIDATE_CODE_OPERATION | USE_EXPANSION
code_lookup_mode: USE_VALIDATE_CODE_OPERATION # AUTO | USE_VALIDATE_CODE_OPERATION | USE_CODESYSTEM_URL
data:
search_parameter_mode: USE_SEARCH_PARAMETERS # AUTO, USE_SEARCH_PARAMETERS, FILTER_IN_MEMORY
terminology_parameter_mode: FILTER_IN_MEMORY # AUTO, USE_VALUE_SET_URL, USE_INLINE_CODES, FILTER_IN_MEMORY
profile_mode: DECLARED # ENFORCED, DECLARED, OPTIONAL, TRUST, OFF
search_parameter_mode: USE_SEARCH_PARAMETERS # AUTO | USE_SEARCH_PARAMETERS | FILTER_IN_MEMORY
terminology_parameter_mode: FILTER_IN_MEMORY # AUTO | USE_VALUE_SET_URL | USE_INLINE_CODES | FILTER_IN_MEMORY
profile_mode: DECLARED # ENFORCED | DECLARED | OPTIONAL | TRUST | OFF
cdshooks:
enabled: false
clientIdHeaderName: client_id
### This enables the swagger-ui at /fhir/swagger-ui/index.html as well as the /fhir/api-docs (see https://hapifhir.io/hapi-fhir/docs/server_plain/openapi.html)
openapi_enabled: true
### This is the FHIR version. Choose between, DSTU2, DSTU3, R4 or R5
fhir_version: R4
### Flag is false by default. This flag enables runtime installation of IG's.
ig_runtime_upload_enabled: false
### This flag when enabled to true, will avail evaluate measure operations from CR Module.
# -------------------------------------------------------------------------------
# D. Search & Indexing
# -------------------------------------------------------------------------------
# NOTE: Extended Lucene/Elasticsearch indexing is experimental.
# See https://hapifhir.io/hapi-fhir/docs/server_jpa/elastic.html
advanced_lucene_indexing: false
search_index_full_text_enabled: false
# language_search_parameter_enabled: true
# upliftedRefchains_enabled: true
# index_storage_optimized: false
# enable_index_missing_fields: false
# enable_index_of_type: true
# enable_index_contained_resource: false
# store_resource_in_lucene_index_enabled: true
### enable to use the ApacheProxyAddressStrategy which uses X-Forwarded-* headers
### to determine the FHIR server address
# use_apache_address_strategy: false
### forces the use of the https:// protocol for the returned server address.
### alternatively, it may be set using the X-Forwarded-Proto header.
# use_apache_address_strategy_https: false
### enables the server to overwrite defaults on HTML, css, etc. under the url pattern of eg. /content/custom **
### Folder with custom content MUST be named custom. If omitted then default content applies
#custom_content_path: ./custom
### enables the server host custom content. If e.g. the value ./configs/app is supplied then the content
### will be served under /web/app
#app_content_path: ./configs/app
### enable to set the Server URL
# server_address: http://hapi.fhir.org/baseR4
# defer_indexing_for_codesystems_of_size: 101
### Flag is true by default. This flag filters resources during package installation, allowing only those resources with a valid status (e.g. active) to be installed.
# validate_resource_status_for_package_upload: false
# install_transitive_ig_dependencies: true
#implementationguides:
### example from registry (packages.fhir.org)
# swiss:
# name: swiss.mednet.fhir
# version: 0.8.0
# reloadExisting: false
# installMode: STORE_AND_INSTALL
# example not from registry
# ips_1_0_0:
# packageUrl: https://build.fhir.org/ig/HL7/fhir-ips/package.tgz
# name: hl7.fhir.uv.ips
# version: 1.0.0
# supported_resource_types:
# - Patient
# - Observation
##################################################
# Allowed Bundle Types for persistence (defaults are: COLLECTION,DOCUMENT,MESSAGE)
##################################################
# allowed_bundle_types: COLLECTION,DOCUMENT,MESSAGE,TRANSACTION,TRANSACTIONRESPONSE,BATCH,BATCHRESPONSE,HISTORY,SEARCHSET
# -------------------------------------------------------------------------------
# E. Bulk Operations
# -------------------------------------------------------------------------------
bulk_export_enabled: false
bulk_import_enabled: false
# -------------------------------------------------------------------------------
# F. Write / Delete / Integrity
# -------------------------------------------------------------------------------
# allow_cascading_deletes: true
# allow_contains_searches: true
# allow_external_references: true
@@ -189,54 +268,45 @@ hapi:
# allow_override_default_search_params: true
# auto_create_placeholder_reference_targets: false
# mass_ingestion_mode_enabled: false
### tells the server to automatically append the current version of the target resource to references at these paths
# auto_version_reference_at_paths: Device.patient, Device.location, Device.parent, DeviceMetric.parent, DeviceMetric.source, Observation.device, Observation.subject
# ips_enabled: false
# default_encoding: JSON
# default_pretty_print: true
# default_page_size: 20
# delete_enabled: true
# delete_expunge_enabled: true
# match_url_cache_enabled: false
# enable_repository_validating_interceptor: true
### Reduce the size used by search indexes by not tagging every row with the resource type and parameter name (this setting makes manual inspection of the database more difficult, but does not impact HAPI FHIR functionality in any way)
# index_storage_optimized: false
# enable_index_missing_fields: false
# enable_index_of_type: true
# enable_index_contained_resource: false
# upliftedRefchains_enabled: true
# resource_dbhistory_enabled: false
### !!Extended Lucene/Elasticsearch Indexing is still a experimental feature, expect some features (e.g. _total=accurate) to not work as expected!!
### more information here: https://hapifhir.io/hapi-fhir/docs/server_jpa/elastic.html
advanced_lucene_indexing: false
search_index_full_text_enabled: false
bulk_export_enabled: false
bulk_import_enabled: false
# language_search_parameter_enabled: true
# enforce_referential_integrity_on_delete: false
# This is an experimental feature, and does not fully support _total and other FHIR features.
# client_id_strategy: ALPHANUMERIC
# server_id_strategy: SEQUENTIAL_NUMERIC
# enforce_referential_integrity_on_delete: false
# enforce_referential_integrity_on_write: false
# etag_support_enabled: true
# expunge_enabled: true
# client_id_strategy: ALPHANUMERIC
# server_id_strategy: SEQUENTIAL_NUMERIC
# fhirpath_interceptor_enabled: false
# filter_search_enabled: true
# graphql_enabled: true
# Thread pool configuration for maintenance operations
# Defaults to Runtime.getRuntime().availableProcessors() if not specified
# reindex_thread_count: 4 # Number of threads to use for reindex operations
# expunge_thread_count: 4 # Number of threads to use for expunge operations
# -------------------------------------------------------------------------------
# G. Narrative & Validation
# -------------------------------------------------------------------------------
narrative_enabled: false
# validation:
# requests_enabled: true
# responses_enabled: true
# -------------------------------------------------------------------------------
# H. MDM (Master Data Management)
# -------------------------------------------------------------------------------
mdm_enabled: false
mdm_rules_json_location: "mdm-rules.json"
## see: https://hapifhir.io/hapi-fhir/docs/interceptors/built_in_server_interceptors.html#jpa-server-retry-on-version-conflicts
# userRequestRetryVersionConflictsInterceptorEnabled: false
# local_base_urls:
# - https://hapi.fhir.org/baseR4
# -------------------------------------------------------------------------------
# I. Terminology / ValueSet Expansion
# -------------------------------------------------------------------------------
# pre_expand_value_sets: true
# enable_task_pre_expand_value_sets: true
# pre_expand_value_sets_default_count: 1000
# pre_expand_value_sets_max_count: 1000
# maximum_expansion_size: 1000
logical_urls:
- http://terminology.hl7.org/*
- https://terminology.hl7.org/*
@@ -247,38 +317,32 @@ hapi:
- http://loinc.org/*
- https://loinc.org/*
### Uncomment the following section, and any sub-properties you need in order to enable
### partitioning support on this server.
# -------------------------------------------------------------------------------
# J. Partitioning & Multitenancy
# -------------------------------------------------------------------------------
# partitioning:
# allow_references_across_partitions: false
# partitioning_include_in_search_hashes: false
# default_partition_id: 0
### Enable the following setting to enable Database Partitioning Mode
### See: https://hapifhir.io/hapi-fhir/docs/server_jpa_partitioning/db_partition_mode.html
# database_partition_mode_enabled: true
### Partition Style: Partitioning requires a partition interceptor which helps the server
### select which partition(s) should be accessed for a given request. You can supply your
### own interceptor (see https://hapifhir.io/hapi-fhir/docs/server_jpa_partitioning/partitioning.html#partition-interceptors )
### but the following setting can also be used to use a built-in form.
### Patient ID Partitioning Mode uses the patient/subject ID to determine the partition
# patient_id_partitioning_mode: true
### Request tenant mode can be used for a multi-tenancy setup where the request path is
### expected to have an additional path element, e.g. GET http://example.com/fhir/TENANT-ID/Patient/A
# request_tenant_partitioning_mode: false
# -------------------------------------------------------------------------------
# K. CORS
# -------------------------------------------------------------------------------
cors:
allow_Credentials: true
# These are allowed_origin patterns, see: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/cors/CorsConfiguration.html#setAllowedOriginPatterns-java.util.List-
allowed_origin:
- '*'
- "*"
# Search coordinator thread pool sizes
# -------------------------------------------------------------------------------
# L. Search Orchestration
# -------------------------------------------------------------------------------
search-coord-core-pool-size: 20
search-coord-max-pool-size: 100
search-coord-queue-capacity: 200
# Search Prefetch Thresholds.
# This setting sets the number of search results to prefetch. For example, if this list
# is set to [100, 1000, -1] then the server will initially load 100 results and not
# attempt to load more. If the user requests subsequent page(s) of results and goes
@@ -286,28 +350,27 @@ hapi:
# The system will progressively work through these thresholds.
# A threshold of -1 means to load all results. Note that if the final threshold is a
# number other than -1, the system will never prefetch more than the given number.
# CSV list; -1 as final value means "all"
search_prefetch_thresholds: 13,503,2003,-1
# -------------------------------------------------------------------------------
# M. Extensibility (custom beans / interceptors / providers)
# -------------------------------------------------------------------------------
# comma-separated package names, will be @ComponentScan'ed by Spring to allow for creating custom Spring beans
#custom-bean-packages:
# comma-separated list of fully qualified interceptor classes.
# classes listed here will be fetched from the Spring context when combined with 'custom-bean-packages',
# or will be instantiated via reflection using a no-arg constructor; then registered with the server
#custom-interceptor-classes:
# comma-separated list of fully qualified provider classes.
# classes listed here will be fetched from the Spring context when combined with 'custom-bean-packages',
# or will be instantiated via reflection using a no-arg constructor; then registered with the server
# custom-provider-classes:
# specify what should be stored in meta.source based on StoreMetaSourceInformationEnum defaults to NONE
# custom-interceptor-classes:
# custom-provider-classes:
# store_meta_source_information: NONE
# Threadpool size for BATCH'ed GETs in a bundle.
# bundle_batch_pool_size: 10
# bundle_batch_pool_max_size: 50
# -------------------------------------------------------------------------------
# N. Logging
# -------------------------------------------------------------------------------
# logger:
# error_format: 'ERROR - ${requestVerb} ${requestUrl}'
# error_format: "ERROR - ${requestVerb} ${requestUrl}"
# format: >-
# Path[${servletPath}] Source[${requestHeader.x-forwarded-for}]
# Operation[${operationType} ${operationName} ${idOrResourceName}]
@@ -315,39 +378,41 @@ hapi:
# ResponseEncoding[${responseEncodingNoDefault}]
# log_exceptions: true
# name: fhirtest.access
# -------------------------------------------------------------------------------
# O. Storage / Pagination / Caching
# -------------------------------------------------------------------------------
# max_binary_size: 104857600
# max_page_size: 200
# retain_cached_searches_mins: 60
# reuse_cached_search_results_millis: 60000
# The remote_terminology_service block is commented out by default because it requires external terminology service endpoints.
# Uncomment and configure the block below if you need to enable remote terminology validation or mapping.
#remote_terminology_service:
# all:
# system: '*'
# url: 'https://tx.fhir.org/r4/'
# snomed:
# system: 'http://snomed.info/sct'
# url: 'https://tx.fhir.org/r4/'
# loinc:
# system: 'http://loinc.org'
# url: 'https://hapi.fhir.org/baseR4/'
tester:
home:
name: Local Tester
server_address: 'http://localhost:8080/fhir'
refuse_to_fetch_third_party_urls: false
fhir_version: R4
global:
name: Global Tester
server_address: "http://hapi.fhir.org/baseR4"
refuse_to_fetch_third_party_urls: false
fhir_version: R4
# validation:
# requests_enabled: true
# responses_enabled: true
# binary_storage_enabled: true
# binary_storage_mode: FILESYSTEM
# binary_storage_filesystem_base_directory: /binstore
# When binary_storage_mode is FILESYSTEM and this value is not set,
# the starter defaults to 102400 bytes so smaller binaries stay inline.
inline_resource_storage_below_size: 4000
# bulk_export_enabled: true
# -------------------------------------------------------------------------------
# P. Remote Terminology Service (disabled by default)
# -------------------------------------------------------------------------------
# remote_terminology_service:
# all:
# system: "*"
# url: "https://tx.fhir.org/r4/"
# snomed:
# system: "http://snomed.info/sct"
# url: "https://tx.fhir.org/r4/"
# loinc:
# system: "http://loinc.org"
# url: "https://hapi.fhir.org/baseR4/"
# -------------------------------------------------------------------------------
# Q. Subscriptions (disabled by default)
# -------------------------------------------------------------------------------
# subscription:
# resthook_enabled: true
# websocket_enabled: false
@@ -363,21 +428,8 @@ hapi:
# startTlsEnable:
# startTlsRequired:
# quitWait:
# -------------------------------------------------------------------------------
# R. LastN (analytics)
# -------------------------------------------------------------------------------
# lastn_enabled: true
# store_resource_in_lucene_index_enabled: true
### This is configuration for normalized quantity search level default is 0
### 0: NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED - default
### 1: NORMALIZED_QUANTITY_STORAGE_SUPPORTED
### 2: NORMALIZED_QUANTITY_SEARCH_SUPPORTED
# normalized_quantity_search_level: 2
#elasticsearch:
# debug:
# pretty_print_json_log: false
# refresh_after_write: false
# enabled: false
# password: SomePassword
# required_index_status: YELLOW
# rest_url: 'localhost:9200'
# protocol: 'http'
# schema_management_strategy: CREATE
# username: SomeUsername

View File

@@ -1,332 +0,0 @@
#Uncomment the "servlet" and "context-path" lines below to make the fhir endpoint available at /example/path/fhir instead of the default value of /fhir
server:
# servlet:
# context-path: /example/path
port: 8080
#Adds the option to go to e.g. http://localhost:8080/actuator/health for seeing the running configuration
#see https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints
management:
#The following configuration will enable the actuator endpoints at /actuator/health, /actuator/info, /actuator/prometheus, /actuator/metrics. For security purposes, only /actuator/health is enabled by default.
endpoints:
enabled-by-default: false
web:
exposure:
include: 'health' # or e.g. 'info,health,prometheus,metrics' or '*' for all
endpoint:
info:
enabled: true
metrics:
enabled: true
health:
enabled: true
probes:
enabled: true
group:
liveness:
include:
- livenessState
- readinessState
prometheus:
enabled: true
prometheus:
metrics:
export:
enabled: true
spring:
main:
allow-circular-references: true
allow-bean-definition-overriding: true
flyway:
enabled: false
baselineOnMigrate: true
fail-on-missing-locations: false
datasource:
#url: 'jdbc:h2:file:./target/database/h2'
url: jdbc:h2:mem:test_mem
username: sa
password: null
driverClassName: org.h2.Driver
max-active: 15
# database connection pool size
hikari:
maximum-pool-size: 10
jpa:
properties:
hibernate.format_sql: false
hibernate.show_sql: false
#Hibernate dialect is automatically detected except Postgres and H2.
#If using H2, then supply the value of ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
#If using postgres, then supply the value of ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgres94Dialect
hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
# hibernate.hbm2ddl.auto: update
# hibernate.jdbc.batch_size: 20
# hibernate.cache.use_query_cache: false
# hibernate.cache.use_second_level_cache: false
# hibernate.cache.use_structured_entries: false
# hibernate.cache.use_minimal_puts: false
### These settings will enable fulltext search with lucene or elastic
hibernate.search.enabled: true
### lucene parameters
# hibernate.search.backend.type: lucene
# hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiLuceneAnalysisConfigurer
# hibernate.search.backend.directory.type: local-filesystem
# hibernate.search.backend.directory.root: target/lucenefiles
# hibernate.search.backend.lucene_version: lucene_current
### elastic parameters ===> see also elasticsearch section below <===
# hibernate.search.backend.type: elasticsearch
# hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer
hapi:
fhir:
### This flag when enabled to true, will avail evaluate measure operations from CR Module.
### Flag is false by default, can be passed as command line argument to override.
cr:
enabled: true
caregaps:
reporter: "default"
section_author: "default"
terminologyServerClientSettings:
maxRetryCount: 3
retryIntervalMillis: 1000
timeoutSeconds: 30
socketTimeout: 60
cql:
use_embedded_libraries: true
compiler:
### These are low-level compiler options.
### They are not typically needed by most users.
# validate_units: true
# verify_only: false
# compatibility_level: "1.5"
error_level: Info
signature_level: All
# analyze_data_requirements: false
# collapse_data_requirements: false
# translator_format: JSON
# enable_date_range_optimization: true
enable_annotations: true
enable_locators: true
enable_results_type: true
enable_detailed_errors: true
# disable_list_traversal: false
# disable_list_demotion: false
# enable_interval_demotion: false
# enable_interval_promotion: false
# disable_method_invocation: false
# require_from_keyword: false
# disable_default_model_info_load: false
runtime:
debug_logging_enabled: false
# enable_validation: false
# enable_expression_caching: true
terminology:
valueset_preexpansion_mode: REQUIRE # USE_IF_PRESENT, REQUIRE, IGNORE
valueset_expansion_mode: PERFORM_NAIVE_EXPANSION # AUTO, USE_EXPANSION_OPERATION, PERFORM_NAIVE_EXPANSION
valueset_membership_mode: USE_EXPANSION # AUTO, USE_VALIDATE_CODE_OPERATION, USE_EXPANSION
code_lookup_mode: USE_CODESYSTEM_URL # AUTO, USE_VALIDATE_CODE_OPERATION, USE_CODESYSTEM_URL
data:
search_parameter_mode: USE_SEARCH_PARAMETERS # AUTO, USE_SEARCH_PARAMETERS, FILTER_IN_MEMORY
terminology_parameter_mode: FILTER_IN_MEMORY # AUTO, USE_VALUE_SET_URL, USE_INLINE_CODES, FILTER_IN_MEMORY
profile_mode: DECLARED # ENFORCED, DECLARED, OPTIONAL, TRUST, OFF
cdshooks:
enabled: true
clientIdHeaderName: client_id
### This enables the swagger-ui at /fhir/swagger-ui/index.html as well as the /fhir/api-docs (see https://hapifhir.io/hapi-fhir/docs/server_plain/openapi.html)
openapi_enabled: true
### This is the FHIR version. Choose between, DSTU2, DSTU3, R4 or R5
fhir_version: R4
### Flag is false by default. This flag enables runtime installation of IG's.
ig_runtime_upload_enabled: false
### This flag when enabled to true, will avail evaluate measure operations from CR Module.
### enable to use the ApacheProxyAddressStrategy which uses X-Forwarded-* headers
### to determine the FHIR server address
# use_apache_address_strategy: false
### forces the use of the https:// protocol for the returned server address.
### alternatively, it may be set using the X-Forwarded-Proto header.
# use_apache_address_strategy_https: false
### enables the server to overwrite defaults on HTML, css, etc. under the url pattern of e.g. /content/custom **
### Folder with custom content MUST be named custom. If omitted then default content applies
#custom_content_path: ./custom
### enables the server host custom content. If e.g. the value ./configs/app is supplied then the content
### will be served under /web/app
#app_content_path: ./configs/app
### enable to set the Server URL
# server_address: http://hapi.fhir.org/baseR4
# defer_indexing_for_codesystems_of_size: 101
### Flag is true by default. This flag filters resources during package installation, allowing only those resources with a valid status (e.g. active) to be installed.
# validate_resource_status_for_package_upload: false
# install_transitive_ig_dependencies: true
#implementationguides:
### example from registry (packages.fhir.org)
# swiss:
# name: swiss.mednet.fhir
# version: 0.8.0
# reloadExisting: false
# installMode: STORE_AND_INSTALL
# example not from registry
# ips_1_0_0:
# packageUrl: https://build.fhir.org/ig/HL7/fhir-ips/package.tgz
# name: hl7.fhir.uv.ips
# version: 1.0.0
# supported_resource_types:
# - Patient
# - Observation
##################################################
# Allowed Bundle Types for persistence (defaults are: COLLECTION,DOCUMENT,MESSAGE)
##################################################
# allowed_bundle_types: COLLECTION,DOCUMENT,MESSAGE,TRANSACTION,TRANSACTIONRESPONSE,BATCH,BATCHRESPONSE,HISTORY,SEARCHSET
# allow_cascading_deletes: true
# allow_contains_searches: true
allow_external_references: true
# allow_multiple_delete: true
# allow_override_default_search_params: true
# auto_create_placeholder_reference_targets: false
### tells the server to automatically append the current version of the target resource to references at these paths
# auto_version_reference_at_paths: Device.patient, Device.location, Device.parent, DeviceMetric.parent, DeviceMetric.source, Observation.device, Observation.subject
# ips_enabled: false
# default_encoding: JSON
# default_pretty_print: true
# default_page_size: 20
# delete_expunge_enabled: true
# enable_repository_validating_interceptor: true
# enable_index_missing_fields: false
# enable_index_of_type: true
# enable_index_contained_resource: false
# resource_dbhistory_enabled: false
### !!Extended Lucene/Elasticsearch Indexing is still an experimental feature, expect some features (e.g. _total=accurate) to not work as expected!!
### more information here: https://hapifhir.io/hapi-fhir/docs/server_jpa/elastic.html
advanced_lucene_indexing: false
search_index_full_text_enabled: false
bulk_export_enabled: false
bulk_import_enabled: false
# language_search_parameter_enabled: true
# enforce_referential_integrity_on_delete: false
# This is an experimental feature, and does not fully support _total and other FHIR features.
# enforce_referential_integrity_on_delete: false
# enforce_referential_integrity_on_write: false
# etag_support_enabled: true
# expunge_enabled: true
# client_id_strategy: ALPHANUMERIC
# server_id_strategy: SEQUENTIAL_NUMERIC
# fhirpath_interceptor_enabled: false
# filter_search_enabled: true
# graphql_enabled: true
narrative_enabled: false
mdm_enabled: false
mdm_rules_json_location: "mdm-rules.json"
# local_base_urls:
# - https://hapi.fhir.org/baseR4
# pre_expand_value_sets: true
# enable_task_pre_expand_value_sets: true
# pre_expand_value_sets_default_count: 1000
# pre_expand_value_sets_max_count: 1000
# maximum_expansion_size: 1000
logical_urls:
- http://terminology.hl7.org/*
- https://terminology.hl7.org/*
- http://snomed.info/*
- https://snomed.info/*
- http://unitsofmeasure.org/*
- https://unitsofmeasure.org/*
- http://loinc.org/*
- https://loinc.org/*
# partitioning:
# allow_references_across_partitions: false
# partitioning_include_in_search_hashes: false
# conditional_create_duplicate_identifiers_enabled: false
cors:
allow_Credentials: true
# These are allowed_origin patterns, see: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/cors/CorsConfiguration.html#setAllowedOriginPatterns-java.util.List-
allowed_origin:
- '*'
# Search coordinator thread pool sizes
search-coord-core-pool-size: 20
search-coord-max-pool-size: 100
search-coord-queue-capacity: 200
# comma-separated package names, will be @ComponentScan'ed by Spring to allow for creating custom Spring beans
#custom-bean-packages:
# comma-separated list of fully qualified interceptor classes.
# classes listed here will be fetched from the Spring context when combined with 'custom-bean-packages',
# or will be instantiated via reflection using a no-arg constructor; then registered with the server
#custom-interceptor-classes:
# comma-separated list of fully qualified provider classes.
# classes listed here will be fetched from the Spring context when combined with 'custom-bean-packages',
# or will be instantiated via reflection using a no-arg constructor; then registered with the server
#custom-provider-classes:
# Threadpool size for BATCH'ed GETs in a bundle.
# bundle_batch_pool_size: 10
# bundle_batch_pool_max_size: 50
# logger:
# error_format: 'ERROR - ${requestVerb} ${requestUrl}'
# format: >-
# Path[${servletPath}] Source[${requestHeader.x-forwarded-for}]
# Operation[${operationType} ${operationName} ${idOrResourceName}]
# UA[${requestHeader.user-agent}] Params[${requestParameters}]
# ResponseEncoding[${responseEncodingNoDefault}]
# log_exceptions: true
# name: fhirtest.access
# max_binary_size: 104857600
# max_page_size: 200
# retain_cached_searches_mins: 60
# reuse_cached_search_results_millis: 60000
tester:
home:
name: Local Tester
server_address: 'http://localhost:8080/fhir'
refuse_to_fetch_third_party_urls: false
fhir_version: R4
global:
name: Global Tester
server_address: "http://hapi.fhir.org/baseR4"
refuse_to_fetch_third_party_urls: false
fhir_version: R4
# validation:
# requests_enabled: true
# responses_enabled: true
# binary_storage_enabled: true
inline_resource_storage_below_size: 4000
# bulk_export_enabled: true
# subscription:
# resthook_enabled: true
# websocket_enabled: false
# email:
# from: some@test.com
# host: google.com
# port:
# username:
# password:
# auth:
# startTlsEnable:
# startTlsRequired:
# quitWait:
# lastn_enabled: true
# store_resource_in_lucene_index_enabled: true
### This is configuration for normalized quantity search level default is 0
### 0: NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED - default
### 1: NORMALIZED_QUANTITY_STORAGE_SUPPORTED
### 2: NORMALIZED_QUANTITY_SEARCH_SUPPORTED
# normalized_quantity_search_level: 2
#elasticsearch:
# debug:
# pretty_print_json_log: false
# refresh_after_write: false
# enabled: false
# password: SomePassword
# required_index_status: YELLOW
# rest_url: 'localhost:9200'
# protocol: 'http'
# schema_management_strategy: CREATE
# username: SomeUsername

View File

@@ -0,0 +1,324 @@
package ca.uhn.fhir.jpa.starter;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.binstore.FilesystemBinaryStorageSvcImpl;
import ca.uhn.fhir.jpa.dao.data.IBinaryStorageEntityDao;
import ca.uhn.fhir.jpa.model.entity.BinaryStorageEntity;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
abstract class BaseBinaryStorageIntegrationTest {
protected static final String COMMON_CONFIG_LOCATION = "spring.config.location=classpath:/binary-storage-test-empty.yaml";
protected static final String COMMON_H2_USERNAME = "spring.datasource.username=sa";
protected static final String COMMON_H2_PASSWORD = "spring.datasource.password=";
protected static final String COMMON_JPA_DDL = "spring.jpa.hibernate.ddl-auto=create-drop";
protected static final String COMMON_HIBERNATE_DIALECT =
"spring.jpa.properties.hibernate.dialect=ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect";
protected static final String COMMON_HIBERNATE_SEARCH_DISABLED = "spring.jpa.properties.hibernate.search.enabled=false";
protected static final String COMMON_FLYWAY_DISABLED = "spring.flyway.enabled=false";
protected static final String COMMON_FHIR_VERSION = "hapi.fhir.fhir_version=r4";
protected static final String COMMON_REPO_VALIDATION_DISABLED =
"hapi.fhir.enable_repository_validating_interceptor=false";
protected static final String COMMON_MDM_DISABLED = "hapi.fhir.mdm_enabled=false";
protected static final String COMMON_CR_DISABLED = "hapi.fhir.cr_enabled=false";
protected static final String COMMON_SUBSCRIPTION_WS_DISABLED = "hapi.fhir.subscription.websocket_enabled=false";
protected static final String COMMON_BEAN_OVERRIDE_ALLOWED = "spring.main.allow-bean-definition-overriding=true";
protected static final String COMMON_CIRCULAR_REFERENCES = "spring.main.allow-circular-references=true";
protected static final String COMMON_MCP_DISABLED = "spring.ai.mcp.server.enabled=false";
protected static final String CONTENT_TYPE = "application/octet-stream";
@LocalServerPort
protected int port;
protected FhirContext fhirContext;
protected IGenericClient client;
private final List<IIdType> resourcesToDelete = new ArrayList<>();
@BeforeEach
void setUpClient() {
fhirContext = FhirContext.forR4();
fhirContext.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
fhirContext.getRestfulClientFactory().setSocketTimeout(1200 * 1000);
String serverBase = "http://localhost:" + port + "/fhir/";
client = fhirContext.newRestfulGenericClient(serverBase);
resourcesToDelete.clear();
}
@AfterEach
void deleteCreatedResources() {
for (IIdType id : resourcesToDelete) {
try {
client.delete().resourceById(id).execute();
} catch (Exception ignored) {
// Ignore cleanup failures to keep tests resilient
}
}
}
protected IIdType createPatientWithPhoto(String label, byte[] payload) {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:binary-storage-test").setValue(label);
patient.addName().setFamily(label);
patient.addPhoto().setContentType(CONTENT_TYPE).setData(payload);
IIdType id = client.create().resource(patient).execute().getId().toUnqualifiedVersionless();
resourcesToDelete.add(id);
return id;
}
protected String uniqueLabel(String prefix) {
return prefix + "-" + UUID.randomUUID();
}
protected byte[] randomBytes(int size) {
byte[] payload = new byte[size];
ThreadLocalRandom.current().nextBytes(payload);
return payload;
}
protected void assertRegularFileCount(Path baseDir, long expectedFileCount) throws IOException {
assertThat(regularFileCount(baseDir)).isEqualTo(expectedFileCount);
}
protected void assertRegularFileCountGreaterThan(Path baseDir, long minimumFileCount) throws IOException {
assertThat(regularFileCount(baseDir)).isGreaterThan(minimumFileCount);
}
protected Path ensureDirectory(Path directory) throws IOException {
Files.createDirectories(directory);
return directory;
}
protected void deleteDirectoryContents(Path baseDir) throws IOException {
if (Files.notExists(baseDir)) {
return;
}
try (Stream<Path> files = Files.walk(baseDir)) {
files.sorted(Comparator.reverseOrder())
.filter(path -> !path.equals(baseDir))
.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
}
private long regularFileCount(Path baseDir) throws IOException {
if (Files.notExists(baseDir)) {
return 0;
}
try (Stream<Path> files = Files.walk(baseDir)) {
return files.filter(Files::isRegularFile).count();
}
}
}
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = Application.class,
properties = {
BaseBinaryStorageIntegrationTest.COMMON_CONFIG_LOCATION,
"spring.datasource.url=jdbc:h2:mem:binary-storage-db;DB_CLOSE_DELAY=-1",
BaseBinaryStorageIntegrationTest.COMMON_H2_USERNAME,
BaseBinaryStorageIntegrationTest.COMMON_H2_PASSWORD,
BaseBinaryStorageIntegrationTest.COMMON_JPA_DDL,
BaseBinaryStorageIntegrationTest.COMMON_HIBERNATE_DIALECT,
BaseBinaryStorageIntegrationTest.COMMON_HIBERNATE_SEARCH_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_FLYWAY_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_FHIR_VERSION,
BaseBinaryStorageIntegrationTest.COMMON_REPO_VALIDATION_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_MDM_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_CR_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_SUBSCRIPTION_WS_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_BEAN_OVERRIDE_ALLOWED,
BaseBinaryStorageIntegrationTest.COMMON_CIRCULAR_REFERENCES,
BaseBinaryStorageIntegrationTest.COMMON_MCP_DISABLED,
"hapi.fhir.binary_storage_enabled=true",
"hapi.fhir.binary_storage_mode=DATABASE"
}
)
class BinaryStorageDatabaseModeIT extends BaseBinaryStorageIntegrationTest {
@Autowired
private IBinaryStorageEntityDao binaryStorageEntityDao;
@Autowired
private PlatformTransactionManager transactionManager;
private TransactionTemplate transactionTemplate;
@BeforeEach
void initTemplate() {
transactionTemplate = new TransactionTemplate(transactionManager);
}
@Test
void largeAttachmentStoredInDatabase() {
Set<String> beforeIds = captureContentIds();
createPatientWithPhoto(uniqueLabel("database"), randomBytes(150_000));
Set<String> afterIds = captureContentIds();
afterIds.removeAll(beforeIds);
assertThat(afterIds).hasSize(1);
String binaryId = afterIds.iterator().next();
BinaryStorageEntity entity = transactionTemplate.execute(status ->
binaryStorageEntityDao.findById(binaryId).orElseThrow());
assertThat(entity.hasStorageContent()).isTrue();
assertThat(entity.getStorageContentBin()).hasSize(150_000);
transactionTemplate.execute(status -> {
binaryStorageEntityDao.deleteById(binaryId);
return null;
});
}
private Set<String> captureContentIds() {
return transactionTemplate.execute(status ->
binaryStorageEntityDao.findAll().stream()
.map(BinaryStorageEntity::getContentId)
.collect(Collectors.toCollection(LinkedHashSet::new)));
}
}
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = Application.class,
properties = {
BaseBinaryStorageIntegrationTest.COMMON_CONFIG_LOCATION,
"spring.datasource.url=jdbc:h2:mem:binary-storage-fs-default;DB_CLOSE_DELAY=-1",
BaseBinaryStorageIntegrationTest.COMMON_H2_USERNAME,
BaseBinaryStorageIntegrationTest.COMMON_H2_PASSWORD,
BaseBinaryStorageIntegrationTest.COMMON_JPA_DDL,
BaseBinaryStorageIntegrationTest.COMMON_HIBERNATE_DIALECT,
BaseBinaryStorageIntegrationTest.COMMON_HIBERNATE_SEARCH_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_FLYWAY_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_FHIR_VERSION,
BaseBinaryStorageIntegrationTest.COMMON_REPO_VALIDATION_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_MDM_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_CR_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_SUBSCRIPTION_WS_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_BEAN_OVERRIDE_ALLOWED,
BaseBinaryStorageIntegrationTest.COMMON_CIRCULAR_REFERENCES,
BaseBinaryStorageIntegrationTest.COMMON_MCP_DISABLED,
"hapi.fhir.binary_storage_enabled=true",
"hapi.fhir.binary_storage_mode=FILESYSTEM",
"hapi.fhir.binary_storage_filesystem_base_directory=target/test-binary-storage/filesystem-default"
}
)
class BinaryStorageFilesystemDefaultIT extends BaseBinaryStorageIntegrationTest {
static final Path BASE_DIRECTORY = Paths.get("target/test-binary-storage/filesystem-default").toAbsolutePath();
@Autowired
private FilesystemBinaryStorageSvcImpl filesystemBinaryStorageSvc;
@BeforeEach
void prepareDirectory() throws IOException {
ensureDirectory(BASE_DIRECTORY);
deleteDirectoryContents(BASE_DIRECTORY);
}
@Test
void filesystemModeUsesDefaultThreshold() throws IOException {
assertThat(filesystemBinaryStorageSvc.getMinimumBinarySize()).isEqualTo(102_400);
assertRegularFileCount(BASE_DIRECTORY, 0);
createPatientWithPhoto(uniqueLabel("fs-default-inline"), randomBytes(50_000));
assertRegularFileCount(BASE_DIRECTORY, 0);
createPatientWithPhoto(uniqueLabel("fs-default-offload"), randomBytes(150_000));
assertRegularFileCountGreaterThan(BASE_DIRECTORY, 0);
}
@AfterEach
void cleanUpDirectory() throws IOException {
deleteDirectoryContents(BASE_DIRECTORY);
}
}
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = Application.class,
properties = {
BaseBinaryStorageIntegrationTest.COMMON_CONFIG_LOCATION,
"spring.datasource.url=jdbc:h2:mem:binary-storage-fs-custom;DB_CLOSE_DELAY=-1",
BaseBinaryStorageIntegrationTest.COMMON_H2_USERNAME,
BaseBinaryStorageIntegrationTest.COMMON_H2_PASSWORD,
BaseBinaryStorageIntegrationTest.COMMON_JPA_DDL,
BaseBinaryStorageIntegrationTest.COMMON_HIBERNATE_DIALECT,
BaseBinaryStorageIntegrationTest.COMMON_HIBERNATE_SEARCH_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_FLYWAY_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_FHIR_VERSION,
BaseBinaryStorageIntegrationTest.COMMON_REPO_VALIDATION_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_MDM_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_CR_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_SUBSCRIPTION_WS_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_BEAN_OVERRIDE_ALLOWED,
BaseBinaryStorageIntegrationTest.COMMON_CIRCULAR_REFERENCES,
BaseBinaryStorageIntegrationTest.COMMON_MCP_DISABLED,
"hapi.fhir.binary_storage_enabled=true",
"hapi.fhir.binary_storage_mode=FILESYSTEM",
"hapi.fhir.binary_storage_filesystem_base_directory=target/test-binary-storage/filesystem-custom",
"hapi.fhir.inline_resource_storage_below_size=32768"
}
)
class BinaryStorageFilesystemCustomThresholdIT extends BaseBinaryStorageIntegrationTest {
static final Path BASE_DIRECTORY = Paths.get("target/test-binary-storage/filesystem-custom").toAbsolutePath();
@Autowired
private FilesystemBinaryStorageSvcImpl filesystemBinaryStorageSvc;
@BeforeEach
void prepareDirectory() throws IOException {
ensureDirectory(BASE_DIRECTORY);
deleteDirectoryContents(BASE_DIRECTORY);
}
@Test
void filesystemModeHonoursCustomThreshold() throws IOException {
assertThat(filesystemBinaryStorageSvc.getMinimumBinarySize()).isEqualTo(32_768);
assertRegularFileCount(BASE_DIRECTORY, 0);
createPatientWithPhoto(uniqueLabel("fs-custom-inline"), randomBytes(30_000));
assertRegularFileCount(BASE_DIRECTORY, 0);
createPatientWithPhoto(uniqueLabel("fs-custom-offload"), randomBytes(40_000));
assertRegularFileCountGreaterThan(BASE_DIRECTORY, 0);
}
@AfterEach
void cleanUpDirectory() throws IOException {
deleteDirectoryContents(BASE_DIRECTORY);
}
}

View File

@@ -19,6 +19,7 @@ import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.opencds.cqf.fhir.cr.hapi.config.CrCdsHooksConfig;
import org.opencds.cqf.fhir.cr.hapi.config.RepositoryConfig;
@@ -35,24 +36,19 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = {
Application.class,
NicknameServiceConfig.class,
RepositoryConfig.class,
TestCdsHooksConfig.class,
CrCdsHooksConfig.class,
StarterCdsHooksConfig.class
}, properties = {
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class, NicknameServiceConfig.class, RepositoryConfig.class, TestCdsHooksConfig.class, CrCdsHooksConfig.class, StarterCdsHooksConfig.class},
properties = {
"spring.profiles.include=storageSettingsTest",
"spring.datasource.url=jdbc:h2:mem:dbr4",
"spring.jpa.properties.hibernate.search.backend.directory.type=local-heap",
"hapi.fhir.enable_repository_validating_interceptor=true",
"hapi.fhir.fhir_version=r4",
"hapi.fhir.cr.enabled=true",
"hapi.fhir.cr.caregaps.section_author=Organization/alphora-author",
"hapi.fhir.cr.caregaps.reporter=Organization/alphora",
"hapi.fhir.cdshooks.enabled=true",
"spring.main.allow-bean-definition-overriding=true"})
"spring.main.allow-bean-definition-overriding=true",
"server.max-http-request-header-size=16KB"})
class CdsHooksServletIT implements IServerSupport {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CdsHooksServletIT.class);
private final FhirContext ourCtx = FhirContext.forR4Cached();
@@ -161,77 +157,81 @@ class CdsHooksServletIT implements IServerSupport {
loadBundle("r4/opioidcds-10-order-sign-bundle.json", ourCtx, ourClient);
await().atMost(20000, TimeUnit.MILLISECONDS).until(this::hasCdsServices);
var fhirServer = " \"fhirServer\": " + "\"" + ourServerBase + "\"" + ",\n";
var cdsRequest = "{\n" +
" \"hookInstance\": \"055b009c-4a7d-4db4-a35e-0e5198918ed1\",\n" +
" \"hook\": \"order-sign\",\n" +
fhirServer +
" \"context\": {\n" +
" \"patientId\": \"example-rec-10-order-sign-illicit-POS-Cocaine-drugs\",\n" +
" \"userId\": \"COREPRACTITIONER1\",\n" +
" \"draftOrders\": {\n" +
" \"resourceType\": \"Bundle\",\n" +
" \"entry\": [\n" +
" {\n" +
" \"resource\": {\n" +
" \"resourceType\": \"MedicationRequest\",\n" +
" \"id\": \"request-123\",\n" +
" \"status\": \"draft\",\n" +
" \"subject\": {\n" +
" \"reference\": \"Patient/example-rec-10-order-sign-illicit-POS-Cocaine-drugs\"\n" +
" },\n" +
" \"authoredOn\": \"2024-03-27\",\n" +
" \"dosageInstruction\": [\n" +
" {\n" +
" \"timing\": {\n" +
" \"repeat\": {\n" +
" \"frequency\": 1,\n" +
" \"period\": 1,\n" +
" \"periodUnit\": \"d\"\n" +
" }\n" +
" },\n" +
" \"doseAndRate\": [\n" +
" {\n" +
" \"doseQuantity\": {\n" +
" \"value\": 1,\n" +
" \"system\": \"http://unitsofmeasure.org\",\n" +
" \"code\": \"{pill}\"\n" +
" }\n" +
" }\n" +
" ]\n" +
" }\n" +
" ],\n" +
" \"dispenseRequest\": {\n" +
" \"expectedSupplyDuration\": {\n" +
" \"value\": 90,\n" +
" \"unit\": \"days\",\n" +
" \"system\": \"http://unitsofmeasure.org\",\n" +
" \"code\": \"d\"\n" +
" }\n" +
" },\n" +
" \"intent\": \"order\",\n" +
" \"category\": {\n" +
" \"coding\": [\n" +
" {\n" +
" \"system\": \"http://terminology.hl7.org/CodeSystem/medicationrequest-category\",\n" +
" \"code\": \"community\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"medicationCodeableConcept\": {\n" +
" \"coding\": [\n" +
" {\n" +
" \"system\": \"http://www.nlm.nih.gov/research/umls/rxnorm\",\n" +
" \"code\": \"1049502\",\n" +
" \"display\": \"12 HR oxycodone hydrochloride 10 MG Extended Release Oral Tablet\"\n" +
" }\n" +
" ]\n" +
" }\n" +
" }\n" +
" }\n" +
" ]\n" +
" }\n" +
" }\n" +
"}";
var cdsRequest = """
{
"hookInstance": "055b009c-4a7d-4db4-a35e-0e5198918ed1",
"hook": "order-sign",
""" + fhirServer + """
"context": {
"patientId": "example-rec-10-order-sign-illicit-POS-Cocaine-drugs",
"userId": "COREPRACTITIONER1",
"draftOrders": {
"resourceType": "Bundle",
"entry": [
{
"resource": {
"resourceType": "MedicationRequest",
"id": "request-123",
"status": "draft",
"subject": {
"reference": "Patient/example-rec-10-order-sign-illicit-POS-Cocaine-drugs"
},
"authoredOn": "2024-03-27",
"dosageInstruction": [
{
"timing": {
"repeat": {
"frequency": 1,
"period": 1,
"periodUnit": "d"
}
},
"doseAndRate": [
{
"doseQuantity": {
"value": 1,
"system": "http://unitsofmeasure.org",
"code": "{pill}"
}
}
]
}
],
"dispenseRequest": {
"expectedSupplyDuration": {
"value": 90,
"unit": "days",
"system": "http://unitsofmeasure.org",
"code": "d"
}
},
"intent": "order",
"category": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/medicationrequest-category",
"code": "community"
}
]
}
],
"medicationCodeableConcept": {
"coding": [
{
"system": "http://www.nlm.nih.gov/research/umls/rxnorm",
"code": "1049502",
"display": "12 HR oxycodone hydrochloride 10 MG Extended Release Oral Tablet"
}
]
}
}
}
]
}
}
}
""";
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpPost request = new HttpPost(ourCdsBase + "/opioidcds-10-order-sign");
request.setEntity(new StringEntity(cdsRequest));

View File

@@ -15,13 +15,13 @@ import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties = {
"hapi.fhir.custom-bean-packages=some.custom.pkg1",
"spring.jpa.properties.hibernate.search.backend.directory.type=local-heap",
"hapi.fhir.custom-interceptor-classes=some.custom.pkg1.CustomInterceptorBean,some.custom.pkg1.CustomInterceptorPojo",
"spring.datasource.url=jdbc:h2:mem:dbr4",
"hapi.fhir.cr_enabled=false",
// "hapi.fhir.enable_repository_validating_interceptor=true",
"hapi.fhir.fhir_version=r4"
})
class CustomInterceptorTest {
@LocalServerPort

View File

@@ -3,8 +3,8 @@ package ca.uhn.fhir.jpa.starter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.search.lastn.ElasticsearchRestClientFactory;
import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl;
import ca.uhn.fhir.jpa.starter.elastic.ElasticsearchBootSvcImpl;
import ca.uhn.fhir.jpa.test.config.TestElasticsearchContainerHelper;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
@@ -14,8 +14,6 @@ import java.io.IOException;
import java.util.Date;
import java.util.GregorianCalendar;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.indices.IndexSettings;
import jakarta.annotation.PreDestroy;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
@@ -27,6 +25,8 @@ import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@@ -42,6 +42,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
@ExtendWith(SpringExtension.class)
@Testcontainers
@Disabled
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties =
{
"spring.datasource.url=jdbc:h2:mem:dbr4",
@@ -50,19 +51,19 @@ import org.testcontainers.junit.jupiter.Testcontainers;
"hapi.fhir.store_resource_in_lucene_index_enabled=true",
"hapi.fhir.advanced_lucene_indexing=true",
"hapi.fhir.search_index_full_text_enabled=true",
"elasticsearch.enabled=true",
"hapi.fhir.cr_enabled=false",
// Because the port is set randomly, we will set the rest_url using the Initializer.
// "elasticsearch.rest_url='http://localhost:9200'",
"elasticsearch.username=SomeUsername",
"elasticsearch.password=SomePassword",
"elasticsearch.debug.refresh_after_write=true",
"elasticsearch.protocol=http",
"spring.elasticsearch.uris=http://localhost:9200",
"spring.elasticsearch.username=elastic",
"spring.elasticsearch.password=changeme",
"spring.main.allow-bean-definition-overriding=true",
"spring.jpa.properties.hibernate.search.enabled=true",
"spring.jpa.properties.hibernate.search.backend.type=elasticsearch",
"spring.jpa.properties.hibernate.search.backend.analysis.configurer=ca.uhn.fhir.jpa.search.elastic.HapiElasticsearchAnalysisConfigurer"
"spring.jpa.properties.hibernate.search.backend.hosts=localhost:9200",
"spring.jpa.properties.hibernate.search.backend.protocol=http",
"spring.jpa.properties.hibernate.search.backend.analysis.configurer=ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticsearchAnalysisConfigurer"
})
@ContextConfiguration(initializers = ElasticsearchLastNR4IT.Initializer.class)
class ElasticsearchLastNR4IT {
@@ -73,26 +74,26 @@ class ElasticsearchLastNR4IT {
public static ElasticsearchContainer embeddedElastic = TestElasticsearchContainerHelper.getEmbeddedElasticSearch();
@Autowired
private ElasticsearchSvcImpl myElasticsearchSvc;
private ElasticsearchBootSvcImpl myElasticsearchSvc;
@BeforeAll
public static void beforeClass() throws IOException {
//Given
ElasticsearchClient elasticsearchHighLevelRestClient = ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient(
"http", embeddedElastic.getHost() + ":" + embeddedElastic.getMappedPort(9200), "", "");
// ElasticsearchClient elasticsearchHighLevelRestClient = ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient(
// "http", embeddedElastic.getHost() + ":" + embeddedElastic.getMappedPort(9200), "", "");
/* As of 2023-08-10, HAPI FHIR sets SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS to 50000
which is in excess of elastic's default max_result_window. If MAX_SUBSCRIPTION_RESULTS is changed
to a value <= 10000, the following will no longer be necessary. - dotasek
*/
elasticsearchHighLevelRestClient.indices().putTemplate(t->{
/* elasticsearchHighLevelRestClient.indices().putTemplate(t->{
t.name("hapi_fhir_template");
t.indexPatterns("*");
t.settings(new IndexSettings.Builder().maxResultWindow(50000).build());
return t;
});
*/
}
@PreDestroy
@@ -103,7 +104,7 @@ class ElasticsearchLastNR4IT {
@LocalServerPort
private int port;
//@Test
@Test
void testLastN() throws IOException, InterruptedException {
Thread.sleep(2000);
@@ -125,6 +126,7 @@ class ElasticsearchLastNR4IT {
IIdType obsId = ourClient.create().resource(obs).execute().getId().toUnqualifiedVersionless();
myElasticsearchSvc.refreshIndex(ElasticsearchSvcImpl.OBSERVATION_INDEX);
Thread.sleep(2000);
Parameters output = ourClient.operation().onType(Observation.class).named("lastn")
.withParameter(Parameters.class, "max", new IntegerType(1))
@@ -154,7 +156,9 @@ class ElasticsearchLastNR4IT {
public void initialize(
ConfigurableApplicationContext configurableApplicationContext) {
// Since the port is dynamically generated, replace the URL with one that has the correct port
TestPropertyValues.of("elasticsearch.rest_url=" + embeddedElastic.getHost() +":" + embeddedElastic.getMappedPort(9200))
TestPropertyValues.of("spring.elasticsearch.uris=" + embeddedElastic.getHost() +":" + embeddedElastic.getMappedPort(9200))
.applyTo(configurableApplicationContext.getEnvironment());
TestPropertyValues.of("spring.jpa.properties.hibernate.search.backend.hosts=" + embeddedElastic.getHost() +":" + embeddedElastic.getMappedPort(9200))
.applyTo(configurableApplicationContext.getEnvironment());
}

View File

@@ -29,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties =
{
"spring.datasource.url=jdbc:h2:mem:dbr5_dbpm",
"spring.jpa.properties.hibernate.search.backend.directory.type=local-heap",
"hapi.fhir.fhir_version=r5",
"hapi.fhir.partitioning.database_partition_mode_enabled=true",
"hapi.fhir.partitioning.patient_id_partitioning_mode=true"

View File

@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
"hapi.fhir.fhir_version=dstu2",
"spring.datasource.url=jdbc:h2:mem:dbr2",
"hapi.fhir.cr_enabled=false",
"spring.jpa.properties.hibernate.search.backend.directory.type=local-heap"
})
class ExampleServerDstu2IT {

View File

@@ -49,7 +49,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
"hapi.fhir.subscription.websocket_enabled=true",
"hapi.fhir.allow_external_references=true",
"hapi.fhir.allow_placeholder_references=true",
"spring.main.allow-bean-definition-overriding=true"
"spring.main.allow-bean-definition-overriding=true",
"spring.jpa.properties.hibernate.search.backend.directory.type=local-heap"
})
class ExampleServerDstu3IT implements IServerSupport {

View File

@@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
properties = {
"spring.datasource.url=jdbc:h2:mem:dbr4b",
"hapi.fhir.enable_repository_validating_interceptor=true",
"spring.jpa.properties.hibernate.search.backend.directory.type=local-heap",
"hapi.fhir.fhir_version=r4b",
"hapi.fhir.subscription.websocket_enabled=false",
"hapi.fhir.mdm_enabled=false",
@@ -107,7 +108,6 @@ class ExampleServerR4BIT {
}
@BeforeEach
void beforeEach() {

View File

@@ -54,6 +54,7 @@ import static org.opencds.cqf.fhir.utility.r4.Parameters.stringPart;
RepositoryConfig.class
}, properties = {
"spring.datasource.url=jdbc:h2:mem:dbr4",
"spring.ai.mcp.server.enabled=false",
"hapi.fhir.enable_repository_validating_interceptor=true",
"hapi.fhir.fhir_version=r4",
"hapi.fhir.subscription.websocket_enabled=true",
@@ -70,6 +71,9 @@ import static org.opencds.cqf.fhir.utility.r4.Parameters.stringPart;
// beans are ambiguous as they are constructed multiple places. This is evident
// when running in a spring boot environment
"spring.main.allow-bean-definition-overriding=true",
"management.health.elasticsearch.enabled=false",
"spring.jpa.properties.hibernate.search.backend.directory.type=local-heap",
"management.endpoints.web.exposure.include=*",
"hapi.fhir.remote_terminology_service.snomed.system=http://snomed.info/sct",
"hapi.fhir.remote_terminology_service.snomed.url=https://tx.fhir.org/r4"
})

View File

@@ -29,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties =
{
"spring.datasource.url=jdbc:h2:mem:dbr5",
"spring.jpa.properties.hibernate.search.backend.directory.type=local-heap",
"hapi.fhir.fhir_version=r5",
"hapi.fhir.cr_enabled=false",
"hapi.fhir.subscription.websocket_enabled=true",

View File

@@ -0,0 +1,78 @@
package ca.uhn.fhir.jpa.starter;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.searchparam.config.NicknameServiceConfig;
import ca.uhn.fhir.jpa.starter.mcp.ToolFactory;
import ca.uhn.fhir.util.BundleUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.gson.Gson;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.spec.McpSchema;
import org.hl7.fhir.r4.model.Bundle;
import org.junit.jupiter.api.Test;
import org.opencds.cqf.fhir.cr.hapi.config.RepositoryConfig;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class, NicknameServiceConfig.class, RepositoryConfig.class}, properties = {"spring.datasource.url=jdbc:h2:mem:dbr4", "hapi.fhir.fhir_version=r4", "hibernate.search.enabled=true", "spring.ai.mcp.server.enabled=true",})
public class McpTests {
@LocalServerPort
private int port;
@Test
public void mcpTests() throws JsonProcessingException {
var fhirContext = FhirContext.forR4();
var transport = HttpClientStreamableHttpTransport.builder("http://localhost:" + port).endpoint("/mcp/messages").build();
var client = McpClient.sync(transport).requestTimeout(Duration.ofSeconds(10)).capabilities(McpSchema.ClientCapabilities.builder().roots(true) // Enable roots capability
.sampling().build()).build();
var initializationResult = client.initialize();
var tools = client.listTools().tools();
assertThat(tools).isNotEmpty();
var searchToolName = ToolFactory.searchFhirResources().name();
var createToolName = ToolFactory.createFhirResource().name();
assertThat(tools.stream().filter(tool -> tool.name().equals(searchToolName)).findFirst().get()).isNotNull();
assertThat(tools.stream().filter(tool -> tool.name().equals(createToolName)).findFirst().get()).isNotNull();
var createMcpRequest = new McpSchema.CallToolRequest.Builder().arguments(Map.of("operation", "create", "resourceType", "Patient", "resource", """
{
"resourceType": "Patient",
"id": "example",
"identifier": [
{
"system": "urn:something",
"value": "uncleScrooge"
}
]
}""")).name(createToolName).build();
assertThat(client.callTool(createMcpRequest).isError()).isFalse();
var searchMcpRequest = new McpSchema.CallToolRequest.Builder().arguments(Map.of("operation", "search", "resourceType", "Patient", "query", "identifier=urn:something|uncleScrooge")).name(searchToolName).build();
var searchResult = client.callTool(searchMcpRequest);
assertThat(searchResult.isError()).isFalse();
assertThat(searchResult.content().size()).isEqualTo(1);
var content = ((McpSchema.TextContent) searchResult.content().get(0));
var embeddedResponseBundle = new Gson().fromJson(content.text(), LinkedHashMap.class).get("response");
var responseBundle = fhirContext.newJsonParser().parseResource(Bundle.class, embeddedResponseBundle.toString());
var entries = BundleUtil.toListOfEntries(fhirContext, responseBundle);
assertThat(entries.size()).isEqualTo(1);
client.closeGracefully();
}
}

View File

@@ -8,10 +8,9 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.nickname.INicknameSvc;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties = {
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {
"hapi.fhir.fhir_version=r4",
"hapi.fhir.mdm_enabled=true"
})
@@ -19,9 +18,6 @@ class MdmTest {
@Autowired
INicknameSvc nicknameService;
@Autowired
JpaStorageSettings jpaStorageSettings;
@Autowired
SubscriptionSettings subscriptionSettings;

View File

@@ -25,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties =
{
"spring.datasource.url=jdbc:h2:mem:dbr4-mt",
"spring.jpa.properties.hibernate.search.backend.directory.type=local-heap",
"hapi.fhir.fhir_version=r4",
"hapi.fhir.subscription.websocket_enabled=true",
"hapi.fhir.cr_enabled=false",

View File

@@ -30,7 +30,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties = {
"spring.datasource.url=jdbc:h2:mem:dbr4",
"hapi.fhir.fhir_version=r4",
"hapi.fhir.userRequestRetryVersionConflictsInterceptorEnabled=true"
"hapi.fhir.userRequestRetryVersionConflictsInterceptorEnabled=true",
"spring.jpa.properties.hibernate.search.backend.directory.type=local-heap"
})
/**

View File

@@ -0,0 +1,96 @@
package ca.uhn.fhir.jpa.starter.common;
import ca.uhn.fhir.jpa.binstore.DatabaseBinaryContentStorageSvcImpl;
import ca.uhn.fhir.jpa.binstore.FilesystemBinaryStorageSvcImpl;
import ca.uhn.fhir.jpa.binary.api.IBinaryStorageSvc;
import ca.uhn.fhir.jpa.starter.AppProperties;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class FhirServerConfigCommonBinaryStorageTest {
@TempDir
Path tempDir;
private FhirServerConfigCommon newConfig() {
return new FhirServerConfigCommon(new AppProperties());
}
@Test
void defaultsToDatabaseImplementation() {
AppProperties props = new AppProperties();
IBinaryStorageSvc svc = binaryStorageSvc(props);
assertThat(svc).isInstanceOf(DatabaseBinaryContentStorageSvcImpl.class);
}
@Test
void filesystemModeUsesDefaultMinimumWhenUnspecified() throws Exception {
AppProperties props = new AppProperties();
props.setBinary_storage_mode(AppProperties.BinaryStorageMode.FILESYSTEM);
Path baseDir = tempDir.resolve("fs-default");
Files.createDirectories(baseDir);
props.setBinary_storage_filesystem_base_directory(baseDir.toString());
FilesystemBinaryStorageSvcImpl svc = filesystemBinaryStorageSvc(props);
assertThat(svc.getMinimumBinarySize()).isEqualTo(102_400);
}
@Test
void filesystemModeHonoursExplicitMinimum() throws Exception {
AppProperties props = new AppProperties();
props.setBinary_storage_mode(AppProperties.BinaryStorageMode.FILESYSTEM);
props.setInline_resource_storage_below_size(4096);
Path baseDir = tempDir.resolve("fs-min-explicit");
Files.createDirectories(baseDir);
props.setBinary_storage_filesystem_base_directory(baseDir.toString());
FilesystemBinaryStorageSvcImpl svc = filesystemBinaryStorageSvc(props);
assertThat(svc.getMinimumBinarySize()).isEqualTo(4096);
}
@Test
void filesystemModeSupportsZeroMinimumWhenExplicit() throws Exception {
AppProperties props = new AppProperties();
props.setBinary_storage_mode(AppProperties.BinaryStorageMode.FILESYSTEM);
props.setInline_resource_storage_below_size(0);
Path baseDir = tempDir.resolve("fs-zero");
Files.createDirectories(baseDir);
props.setBinary_storage_filesystem_base_directory(baseDir.toString());
FilesystemBinaryStorageSvcImpl svc = filesystemBinaryStorageSvc(props);
assertThat(svc.getMinimumBinarySize()).isZero();
}
@Test
void filesystemModeRequiresBaseDirectory() {
AppProperties props = new AppProperties();
props.setBinary_storage_mode(AppProperties.BinaryStorageMode.FILESYSTEM);
assertThatThrownBy(() -> filesystemBinaryStorageSvc(props))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("binary_storage_filesystem_base_directory");
}
private IBinaryStorageSvc binaryStorageSvc(AppProperties props) {
FhirServerConfigCommon config = newConfig();
if (props.getBinary_storage_mode() == AppProperties.BinaryStorageMode.FILESYSTEM) {
return config.filesystemBinaryStorageSvc(props);
}
return config.databaseBinaryStorageSvc(props);
}
private FilesystemBinaryStorageSvcImpl filesystemBinaryStorageSvc(AppProperties props) {
return (FilesystemBinaryStorageSvcImpl) binaryStorageSvc(props);
}
}

View File

@@ -1,83 +1,4 @@
management:
#The following configuration will enable the actuator endpoints at /actuator/health, /actuator/info, /actuator/prometheus
endpoints:
enabled-by-default: false
web:
exposure:
include: 'info,health,prometheus,metrics' # or '*' for all
endpoint:
info:
enabled: true
metrics:
enabled: true
health:
enabled: true
probes:
enabled: true
group:
liveness:
include:
- livenessState
- readinessState
prometheus:
enabled: true
prometheus:
metrics:
export:
enabled: true
spring:
main:
allow-circular-references: true
allow-bean-definition-overriding: true
flyway:
enabled: false
fail-on-missing-locations: false
baselineOnMigrate: true
datasource:
url: jdbc:h2:mem:test_mem
username: sa
password: null
driverClassName: org.h2.Driver
max-active: 15
# database connection pool size
hikari:
maximum-pool-size: 10
jpa:
properties:
hibernate.format_sql: false
hibernate.show_sql: false
#########################################
# Hibernate Dialect Setting
#########################################
# Use one of the following values:
# ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
# ca.uhn.fhir.jpa.model.dialect.HapiFhirDerbyDialect
# ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect
# ca.uhn.fhir.jpa.model.dialect.HapiFhirOracleDialect
# ca.uhn.fhir.jpa.model.dialect.HapiFhirSQLServerDialect
# ca.uhn.fhir.jpa.model.dialect.HapiFhirMySQLDialect (Deprecated!)
#########################################
hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
#########################################
# hibernate.hbm2ddl.auto: update
# hibernate.jdbc.batch_size: 20
# hibernate.cache.use_query_cache: false
# hibernate.cache.use_second_level_cache: false
# hibernate.cache.use_structured_entries: false
# hibernate.cache.use_minimal_puts: false
### These settings will enable fulltext search with lucene or elastic
hibernate.search.enabled: false
### lucene parameters
# hibernate.search.backend.type: lucene
# hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiLuceneAnalysisConfigurer
# hibernate.search.backend.directory.type: local-filesystem
# hibernate.search.backend.directory.root: target/lucenefiles
# hibernate.search.backend.lucene_version: lucene_current
### elastic parameters ===> see also elasticsearch section below <===
# hibernate.search.backend.type: elasticsearch
# hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer
hapi:
fhir:

View File

@@ -0,0 +1 @@
# Empty config to bypass default application.yaml during BinaryStorageIntegrationTest

View File

@@ -0,0 +1,18 @@
{
"hook": "patient-view",
"hookInstance": "8d5a3a2e-6d8b-4f7c-bb2d-2f1b8cf1d7a1",
"context": {
"userId": "Practitioner/123",
"patientId": "123",
"encounterId": "456"
},
"prefetch": {
"item1": {
"resourceType": "Patient",
"gender": "male",
"birthDate": "1989-10-23",
"id": "123",
"active": true
}
}
}

View File

@@ -0,0 +1,5 @@
{
"userId": "Practitioner/123",
"patientId": "123",
"encounterId": "456"
}

View File

@@ -0,0 +1,9 @@
{
"item1": {
"resourceType": "Patient",
"gender": "male",
"birthDate": "1989-10-23",
"id": "123",
"active": true
}
}

View File

@@ -0,0 +1,42 @@
<PlanDefinition xmlns="http://hl7.org/fhir">
<id value="HelloPatientPd" />
<meta>
<profile value="http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-recommendationdefinition" />
</meta>
<extension url="http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeCapability">
<valueCode value="executable" />
</extension>
<url value="http://example.org/PlanDefinition/HelloPatientPd" />
<identifier>
<use value="official" />
<value value="PlanDefinition_HelloPatientPd" />
</identifier>
<name value="PlanDefinition_HelloPatientPd" />
<title value="PlanDefinition - Hello Patient" />
<type>
<coding>
<system value="http://terminology.hl7.org/CodeSystem/plan-definition-type" />
<code value="eca-rule" />
<display value="ECA Rule" />
</coding>
</type>
<status value="draft" />
<experimental value="true" />
<date value="2024-09-28" />
<description value="Demo PlanDefinition for Hello Patient" />
<action>
<title value="Hello, Patient!" />
<description value="Please state the nature of the medical emergency." />
<trigger>
<type value="named-event" />
<name value="patient-view" />
</trigger>
<condition>
<kind value="applicability" />
<expression>
<language value="text/cql" />
<expression value="true" />
</expression>
</condition>
</action>
</PlanDefinition>