Merge remote-tracking branch 'origin/master' into rel_8_5-tracking
This commit is contained in:
@@ -5,7 +5,6 @@ target/maven-*
|
||||
target/ROOT
|
||||
target/test-classes/
|
||||
target/war
|
||||
target/duplicate-finder-result.xml
|
||||
target/jacoco.exec
|
||||
target/*.original
|
||||
.idea
|
||||
|
||||
5
.github/workflows/spotless-check.yml
vendored
5
.github/workflows/spotless-check.yml
vendored
@@ -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
38
AGENTS.md
Normal 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.
|
||||
@@ -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 && \
|
||||
|
||||
27
README.md
27
README.md
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# HAPI FHIR JPA Server Starter Helm Chart
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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
39
pom.xml
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,48 +8,36 @@ 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) {
|
||||
ourLog.info("Partitioning mode enabled in: Patient ID partitioning mode");
|
||||
PatientIdPartitionInterceptor patientIdInterceptor =
|
||||
new PatientIdPartitionInterceptor(myFhirContext, mySearchParamExtractor, myPartitionSettings);
|
||||
myRestfulServer.registerInterceptor(patientIdInterceptor);
|
||||
myPartitionSettings.setUnnamedPartitionMode(true);
|
||||
} else if (myAppProperties.getPartitioning().getRequest_tenant_partitioning_mode() == Boolean.TRUE) {
|
||||
ourLog.info("Partitioning mode enabled in: Request tenant partitioning mode");
|
||||
myRestfulServer.registerInterceptor(new RequestTenantPartitionInterceptor());
|
||||
myRestfulServer.setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy());
|
||||
}
|
||||
|
||||
myRestfulServer.registerProviders(myPartitionManagementProvider);
|
||||
var partitioning = myAppProperties.getPartitioning();
|
||||
if (partitioning.getPatient_id_partitioning_mode()) {
|
||||
ourLog.info("Partitioning mode enabled in: Patient ID partitioning mode");
|
||||
var patientIdInterceptor = new PatientIdPartitionInterceptor(
|
||||
myRestfulServer.getFhirContext(), mySearchParamExtractor, myPartitionSettings);
|
||||
myRestfulServer.registerInterceptor(patientIdInterceptor);
|
||||
myPartitionSettings.setUnnamedPartitionMode(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());
|
||||
}
|
||||
|
||||
myRestfulServer.registerProviders(myPartitionManagementProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
myConfigurableListableBeanFactory, theFhirContext, theStorageSettings);
|
||||
retVal.setPersistenceUnitName("HAPI_PU");
|
||||
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean =
|
||||
HapiEntityManagerFactoryUtil.newEntityManagerFactory(
|
||||
myConfigurableListableBeanFactory, theFhirContext, theStorageSettings);
|
||||
|
||||
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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
46
src/main/java/ca/uhn/fhir/jpa/starter/cr/CqlData.java
Normal file
46
src/main/java/ca/uhn/fhir/jpa/starter/cr/CqlData.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
34
src/main/java/ca/uhn/fhir/jpa/starter/mcp/Interaction.java
Normal file
34
src/main/java/ca/uhn/fhir/jpa/starter/mcp/Interaction.java
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
120
src/main/java/ca/uhn/fhir/jpa/starter/mcp/RequestBuilder.java
Normal file
120
src/main/java/ca/uhn/fhir/jpa/starter/mcp/RequestBuilder.java
Normal 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;
|
||||
}
|
||||
}
|
||||
337
src/main/java/ca/uhn/fhir/jpa/starter/mcp/ToolFactory.java
Normal file
337
src/main/java/ca/uhn/fhir/jpa/starter/mcp/ToolFactory.java
Normal 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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
9
src/main/java/ca/uhn/fhir/rest/server/McpBridge.java
Normal file
9
src/main/java/ca/uhn/fhir/rest/server/McpBridge.java
Normal 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();
|
||||
}
|
||||
117
src/main/java/ca/uhn/fhir/rest/server/McpCdsBridge.java
Normal file
117
src/main/java/ca/uhn/fhir/rest/server/McpCdsBridge.java
Normal 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;
|
||||
}
|
||||
}
|
||||
101
src/main/java/ca/uhn/fhir/rest/server/McpFhirBridge.java
Normal file
101
src/main/java/ca/uhn/fhir/rest/server/McpFhirBridge.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
427
src/main/resources/application-cds.yaml
Normal file
427
src/main/resources/application-cds.yaml
Normal 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
|
||||
@@ -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:
|
||||
# servlet:
|
||||
# context-path: /example/path
|
||||
# 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"
|
||||
@@ -123,120 +224,89 @@ hapi:
|
||||
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_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
|
||||
|
||||
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
|
||||
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: 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.
|
||||
|
||||
### 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
|
||||
# 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
|
||||
### 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
|
||||
# -------------------------------------------------------------------------------
|
||||
# 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
|
||||
# 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
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
# 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
|
||||
|
||||
# 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
|
||||
# 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
|
||||
# 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/*
|
||||
@@ -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.
|
||||
#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
|
||||
# -------------------------------------------------------------------------------
|
||||
# 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
|
||||
# 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,98 +350,86 @@ 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:
|
||||
# custom-provider-classes:
|
||||
# custom-interceptor-classes:
|
||||
# custom-provider-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
|
||||
# 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
|
||||
# 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
|
||||
# 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
|
||||
# -------------------------------------------------------------------------------
|
||||
# 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
|
||||
# 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
|
||||
# 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:
|
||||
# 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
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 = {
|
||||
"spring.profiles.include=storageSettingsTest",
|
||||
"spring.datasource.url=jdbc:h2:mem:dbr4",
|
||||
"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"})
|
||||
@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",
|
||||
"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));
|
||||
@@ -244,7 +244,7 @@ class CdsHooksServletIT implements IServerSupport {
|
||||
assertNotNull(response);
|
||||
JsonArray cards = response.getAsJsonArray("cards");
|
||||
assertEquals(0, cards.size());
|
||||
// assertEquals("\"Hello World!\"", cards.get(0).getAsJsonObject().get("summary").toString());
|
||||
//assertEquals("\"Hello World!\"", cards.get(0).getAsJsonObject().get("summary").toString());
|
||||
} catch (IOException ioe) {
|
||||
fail(ioe.getMessage());
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||
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",
|
||||
"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"
|
||||
"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
|
||||
|
||||
@@ -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,8 +156,10 @@ 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -17,16 +17,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
classes = {Application.class},
|
||||
properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:dbr4b",
|
||||
"hapi.fhir.enable_repository_validating_interceptor=true",
|
||||
"hapi.fhir.fhir_version=r4b",
|
||||
"hapi.fhir.subscription.websocket_enabled=false",
|
||||
"hapi.fhir.mdm_enabled=false",
|
||||
"hapi.fhir.cr_enabled=false",
|
||||
// Override is currently required when using MDM as the construction of the MDM
|
||||
// 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"})
|
||||
"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",
|
||||
"hapi.fhir.cr_enabled=false",
|
||||
// Override is currently required when using MDM as the construction of the MDM
|
||||
// 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"})
|
||||
class ExampleServerR4BIT {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExampleServerR4BIT.class);
|
||||
private IGenericClient ourClient;
|
||||
@@ -107,7 +108,6 @@ class ExampleServerR4BIT {
|
||||
}
|
||||
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
|
||||
|
||||
@@ -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"
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
78
src/test/java/ca/uhn/fhir/jpa/starter/McpTests.java
Normal file
78
src/test/java/ca/uhn/fhir/jpa/starter/McpTests.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
1
src/test/resources/binary-storage-test-empty.yaml
Normal file
1
src/test/resources/binary-storage-test-empty.yaml
Normal file
@@ -0,0 +1 @@
|
||||
# Empty config to bypass default application.yaml during BinaryStorageIntegrationTest
|
||||
18
src/test/resources/mcp/hello-patient-request.json
Normal file
18
src/test/resources/mcp/hello-patient-request.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/test/resources/mcp/mcp-hookContext-object.json
Normal file
5
src/test/resources/mcp/mcp-hookContext-object.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"userId": "Practitioner/123",
|
||||
"patientId": "123",
|
||||
"encounterId": "456"
|
||||
}
|
||||
9
src/test/resources/mcp/mpc-prefetch-object.json
Normal file
9
src/test/resources/mcp/mpc-prefetch-object.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"item1": {
|
||||
"resourceType": "Patient",
|
||||
"gender": "male",
|
||||
"birthDate": "1989-10-23",
|
||||
"id": "123",
|
||||
"active": true
|
||||
}
|
||||
}
|
||||
42
src/test/resources/mcp/plandefinition-hello-patient.xml
Normal file
42
src/test/resources/mcp/plandefinition-hello-patient.xml
Normal 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>
|
||||
Reference in New Issue
Block a user