Feature/cds config (#857)

* Added MCP support using SSE on http://localhost:8080/sse

* Reverted change that IntelliJ complains about

* Pre-rework

* Cleaned up the code a fair bit

* Renamed

* Renamed

* Running spotless

* Reuse FhirContext in result serialization to make MCP server work with R5

* Added support for transactions

* PoC tool for CDS Hooks

* some cleanup

* Upgrade of model protocol

* Added comments

* Removed field injection ... CDS to be changed to AutoConfig eventually

* Adjusted to new builder pattern

* Update src/main/java/ca/uhn/fhir/rest/server/MCPBridge.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* A bit of restructuring

* More rework

* Removing (suspected unnecessary) formatting

* Add more example doc

* Added a smoke- / passthrough-test

* Applied spotless

* Update src/main/java/ca/uhn/fhir/jpa/starter/mcp/RequestBuilder.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/main/java/ca/uhn/fhir/jpa/starter/mcp/RequestBuilder.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/main/java/ca/uhn/fhir/jpa/starter/mcp/ToolFactory.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/main/java/ca/uhn/fhir/rest/server/McpCdsBridge.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/main/java/ca/uhn/fhir/rest/server/McpCdsBridge.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Formatting

* Added some documentation

* spotless cares about MD?

* Reverting back to default values

* minor refinements

* Fixed CDS hooks configuration

* Fixed some wirings

* Readded missing elements

* getting closer to get test running again ...

* applying review

* Readded exclude

* Bumped spring-ai deps

* added agents file

* Updated according to review

---------

Co-authored-by: Ádám Z. Kövér <adamzkover@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Jens Kristian Villadsen
2025-10-01 22:17:07 +02:00
committed by GitHub
parent d29b9f80af
commit d76662c9e9
20 changed files with 568 additions and 537 deletions

38
AGENTS.md Normal file
View File

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

10
pom.xml
View File

@@ -391,20 +391,14 @@
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp</artifactId> <artifactId>spring-ai-mcp</artifactId>
<version>1.1.0-M1</version> <version>1.1.0-M2</version>
</dependency> </dependency>
<!--implementation("org.springframework.ai:spring-ai-starter-mcp-server-webmvc:1.1.0-M1")--> <!--implementation("org.springframework.ai:spring-ai-starter-mcp-server-webmvc:1.1.0-M1")-->
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId> <artifactId>spring-ai-starter-mcp-server</artifactId>
<version>1.1.0-M1</version> <version>1.1.0-M2</version>
</dependency>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>0.12.1</version>
</dependency> </dependency>
<dependency> <dependency>

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,13 @@
package ca.uhn.fhir.jpa.starter.cdshooks; package ca.uhn.fhir.jpa.starter.cdshooks;
import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.jpa.starter.cr.*;
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.hapi.fhir.cdshooks.api.ICdsHooksDaoAuthorizationSvc; 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 ca.uhn.hapi.fhir.cdshooks.svc.CdsHooksContextBooter;
import org.hl7.fhir.instance.model.api.IBaseResource; 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.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.CrCdsHooksConfig;
import org.opencds.cqf.fhir.cr.hapi.config.RepositoryConfig; 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.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -24,30 +17,9 @@ import org.springframework.context.annotation.Import;
@Configuration @Configuration
@Conditional({CdsHooksConfigCondition.class, CrConfigCondition.class}) @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 { 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 @Bean
public CdsCrSettings cdsCrSettings(CdsHooksProperties cdsHooksProperties) { public CdsCrSettings cdsCrSettings(CdsHooksProperties cdsHooksProperties) {
CdsCrSettings settings = CdsCrSettings.getDefault(); CdsCrSettings settings = CdsCrSettings.getDefault();
@@ -67,8 +39,9 @@ public class StarterCdsHooksConfig {
} }
@Bean @Bean
public ProviderConfiguration providerConfiguration(CdsHooksProperties cdsProperties, CrProperties crProperties) { public ProviderConfiguration providerConfiguration(
return new ProviderConfiguration(cdsProperties, crProperties); CdsHooksProperties cdsProperties, CqlProperties cqlProperties, CqlRuntimeProperties cqlRuntimeProperties) {
return new ProviderConfiguration(cdsProperties, cqlRuntimeProperties);
} }
@Bean @Bean

View File

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

View File

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

View File

@@ -1,5 +1,10 @@
package ca.uhn.fhir.jpa.starter.cr; 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 { public class CareGapsProperties {
private String reporter = "default"; private String reporter = "default";
private String section_author = "default"; private String section_author = "default";

View File

@@ -3,7 +3,11 @@ package ca.uhn.fhir.jpa.starter.cr;
import org.cqframework.cql.cql2elm.CqlCompilerException; import org.cqframework.cql.cql2elm.CqlCompilerException;
import org.cqframework.cql.cql2elm.CqlTranslator; import org.cqframework.cql.cql2elm.CqlTranslator;
import org.cqframework.cql.cql2elm.LibraryBuilder; 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 { public class CqlCompilerProperties {
private Boolean validate_units = true; private Boolean validate_units = true;
private Boolean verify_only = false; private Boolean verify_only = false;

View File

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

View File

@@ -2,14 +2,18 @@ package ca.uhn.fhir.jpa.starter.cr;
import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings;
import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings; 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 { public class CqlProperties {
private Boolean use_embedded_libraries = true; private Boolean use_embedded_libraries = true;
private CqlCompilerProperties compiler = new CqlCompilerProperties(); private CqlCompilerProperties compiler = new CqlCompilerProperties();
private CqlRuntimeProperties runtime = new CqlRuntimeProperties(); private CqlRuntimeProperties runtime = new CqlRuntimeProperties();
private TerminologySettings terminology = new TerminologySettings(); private TerminologySettings terminology = new TerminologySettings();
private RetrieveSettings data = new RetrieveSettings(); private CqlData data = new CqlData();
public Boolean getUse_embedded_libraries() { public Boolean getUse_embedded_libraries() {
return use_embedded_libraries; return use_embedded_libraries;
@@ -43,11 +47,15 @@ public class CqlProperties {
this.terminology = terminology; this.terminology = terminology;
} }
public RetrieveSettings getData() { public CqlData getData() {
return data; return data;
} }
public void setData(RetrieveSettings data) { public void setData(CqlData data) {
this.data = data; this.data = data;
} }
public RetrieveSettings getRetrieveSettings() {
return data.getRetrieveSettings();
}
} }

View File

@@ -1,5 +1,10 @@
package ca.uhn.fhir.jpa.starter.cr; 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 { public class CqlRuntimeProperties {
private Boolean debug_logging_enabled = false; private Boolean debug_logging_enabled = false;

View File

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

View File

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

View File

@@ -1,13 +1,16 @@
package ca.uhn.fhir.jpa.starter.cr; package ca.uhn.fhir.jpa.starter.cr;
import org.opencds.cqf.fhir.utility.client.TerminologyServerClientSettings; 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 { public class CrProperties {
private Boolean enabled; private Boolean enabled;
private CareGapsProperties careGaps = new CareGapsProperties(); private CareGapsProperties careGaps = new CareGapsProperties();
private CqlProperties cql = new CqlProperties(); private CqlProperties cql = new CqlProperties();
private TerminologyServerClientSettings terminologyServerClientSettings = new TerminologyServerClientSettings(); private TerminologyServerClientSettings terminologyServerClientSettings = new TerminologyServerClientSettings();
public Boolean getEnabled() { public Boolean getEnabled() {

View File

@@ -8,6 +8,7 @@ import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceRegistry; import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceRegistry;
import ca.uhn.hapi.fhir.cdshooks.module.CdsHooksObjectMapperFactory; import ca.uhn.hapi.fhir.cdshooks.module.CdsHooksObjectMapperFactory;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties;
@@ -66,7 +67,7 @@ public class McpServerConfig {
return HttpServletStreamableServerTransportProvider.builder() return HttpServletStreamableServerTransportProvider.builder()
.disallowDelete(false) .disallowDelete(false)
.mcpEndpoint(properties.getMcpEndpoint()) .mcpEndpoint(properties.getMcpEndpoint())
.objectMapper(new ObjectMapper()) .jsonMapper(new JacksonMcpJsonMapper(new ObjectMapper()))
// .contextExtractor((serverRequest, context) -> context) // .contextExtractor((serverRequest, context) -> context)
.build(); .build();
} }

View File

@@ -1,24 +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: server:
# servlet: # Uncomment to serve FHIR under a non-default context path (e.g., /example/path/fhir)
# context-path: /example/path # servlet:
# context-path: /example/path
port: 8080 port: 8080
tomcat: tomcat:
# allow | as a separator in the URL # allow | as a separator in URLs
relaxed-query-chars: "|" 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: management:
health: # Actuator endpoints: only /actuator/health exposed by default
elasticsearch:
enabled: false
#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: endpoints:
enabled-by-default: false enabled-by-default: false
web: web:
exposure: exposure:
# expose only health (default) — change to [health,info,prometheus,metrics] if you want them reachable include: "health" # or "info,health,prometheus,metrics" or "*" for all
include: health
endpoint: endpoint:
info: info:
enabled: true enabled: true
@@ -30,40 +28,42 @@ management:
enabled: true enabled: true
group: group:
liveness: liveness:
include: include: [ "livenessState", "readinessState" ]
- livenessState
- readinessState
prometheus: prometheus:
enabled: true enabled: true
prometheus: prometheus:
metrics: metrics:
export: export:
enabled: true enabled: true
spring:
ai:
# Run e.g. `npx @modelcontextprotocol/inspector` and connect to http://localhost:8080/mcp/message using Streamable HTTP
# Add the following to the MCP server settings file in e.g. cursor or claude (Desktop applications) for local debugging: spring:
# cursor: # -------------------------------------------------------------------------------
# { # A. Spring AI — Model Context Protocol (MCP)
# "mcpServers": { # -------------------------------------------------------------------------------
# "hapi": { ai:
# "url": "http://localhost:8080/mcp/message" # 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:
# or claude: # {
# { # "mcpServers": {
# "mcpServers": { # "hapi": {
# "hapi": { # "url": "http://localhost:8080/mcp/messages"
# "command": "npx", # }
# "args": [ # }
# "mcp-remote@latest", # }
# "http://localhost:8080/mcp/message" # or claude:
# ] # {
# } # "mcpServers": {
# } # "hapi": {
# } # "command": "npx",
# "args": [
# "mcp-remote@latest",
# "http://localhost:8080/mcp/messages"
# ]
# }
# }
# }
mcp: mcp:
server: server:
@@ -75,48 +75,43 @@ spring:
mcp-endpoint: /mcp/messages mcp-endpoint: /mcp/messages
#schema: # -------------------------------------------------------------------------------
# fhir-enabled: true # B. Core Spring
# fhir: # -------------------------------------------------------------------------------
# base-url: http://localhost:8080/fhir main:
allow-bean-definition-overriding: false
#query: allow-circular-references: true
# prompt:
# template: |
# You are a FHIR assistant. Translate the following question into a valid FHIR RESTful API query:
# "{{query}}"
# Use the provided FHIR schema:
# {{schema}}
#base-url: /api/v1
autoconfigure: autoconfigure:
# This exclude is only needed for setups not using Elasticsearch where the elasticsearch sniff is not needed. # This exclude is only needed for setups not using Elasticsearch where the elasticsearch sniff is not needed.
exclude: org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration exclude: org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration
main:
allow-bean-definition-overriding: false
allow-circular-references: true
flyway: flyway:
enabled: false enabled: false
baseline-on-migrate: true baseline-on-migrate: true
fail-on-missing-locations: false fail-on-missing-locations: false
datasource: datasource:
#url: 'jdbc:h2:file:./target/database/h2' # url: "jdbc:h2:file:./target/database/h2"
url: jdbc:h2:mem:test_mem url: jdbc:h2:mem:test_mem
username: sa username: sa
password: null password: null
driver-class-name: org.h2.Driver driver-class-name: org.h2.Driver
# max-active: 15 # (ignored with HikariCP; use hikari.maximum-pool-size)
# database connection pool size
hikari: hikari:
maximum-pool-size: 10 maximum-pool-size: 10
# elasticsearch:
# uris: http://localhost:9200
# username: elastic
# password: changeme
jpa: jpa:
properties: properties:
hibernate: 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: hbm2ddl:
auto: update auto: update
jdbc: jdbc:
@@ -126,43 +121,73 @@ spring:
use_second_level_cache: false use_second_level_cache: false
use_structured_entries: false use_structured_entries: false
use_minimal_puts: false use_minimal_puts: false
format_sql: false
show_sql: false # --- Hibernate Search (Lucene/Elasticsearch) ---
#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
#dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect
dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
search: search:
enabled: true enabled: false
schema_management: # Lucene backend (default example)
strategy: create # backend:
### lucene parameters # type: lucene
backend: # analysis:
type: lucene # configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiLuceneAnalysisConfigurer
directory: # directory:
type: local-filesystem # type: local-filesystem
root: target/lucenefiles # root: target/lucenefiles
analysis: # lucene_version: lucene_current
configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiLuceneAnalysisConfigurer # Elasticsearch backend (alternative) — see also hapi.fhir.elasticsearch section in docs
# backend:
### elastic parameters ===> see also elasticsearch section below <=== # type: elasticsearch
# backend: # analysis:
# type: elasticsearch # configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer
# discovery: true
# analysis:
# configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer
# hosts: localhost:9200
# protocol: http
# username: elastic
# password: changeme
# refresh_after_write: true
# -------------------------------------------------------------------------------------
# HAPI FHIR — grouped by domain
# -------------------------------------------------------------------------------------
hapi: hapi:
fhir: 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: cr:
enabled: false enabled: false # exposes Clinical Reasoning operation endpoints
caregaps: caregaps:
reporter: "default" reporter: "default"
section_author: "default" section_author: "default"
@@ -174,8 +199,7 @@ hapi:
cql: cql:
use_embedded_libraries: true use_embedded_libraries: true
compiler: compiler:
### These are low-level compiler options. # low-level compiler options (typically not needed)
### They are not typically needed by most users.
# validate_units: true # validate_units: true
# verify_only: false # verify_only: false
# compatibility_level: "1.5" # compatibility_level: "1.5"
@@ -200,124 +224,84 @@ hapi:
debug_logging_enabled: false debug_logging_enabled: false
# enable_validation: false # enable_validation: false
# enable_expression_caching: true # enable_expression_caching: true
terminology: terminology:
valueset_preexpansion_mode: REQUIRE # USE_IF_PRESENT, REQUIRE, IGNORE valueset_preexpansion_mode: REQUIRE # USE_IF_PRESENT | REQUIRE | IGNORE
valueset_expansion_mode: PERFORM_NAIVE_EXPANSION # AUTO, USE_EXPANSION_OPERATION, PERFORM_NAIVE_EXPANSION 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 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 code_lookup_mode: USE_VALIDATE_CODE_OPERATION # AUTO | USE_VALIDATE_CODE_OPERATION | USE_CODESYSTEM_URL
data: data:
search_parameter_mode: USE_SEARCH_PARAMETERS # AUTO, USE_SEARCH_PARAMETERS, FILTER_IN_MEMORY 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 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 profile_mode: DECLARED # ENFORCED | DECLARED | OPTIONAL | TRUST | OFF
cdshooks: cdshooks:
enabled: false enabled: false
clientIdHeaderName: client_id 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 # D. Search & Indexing
### This is the FHIR version. Choose between, DSTU2, DSTU3, R4 or R5 # -------------------------------------------------------------------------------
fhir_version: R4 # NOTE: Extended Lucene/Elasticsearch indexing is experimental.
### Flag is false by default. This flag enables runtime installation of IG's. # See https://hapifhir.io/hapi-fhir/docs/server_jpa/elastic.html
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://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 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
advanced_lucene_indexing: false advanced_lucene_indexing: false
search_index_full_text_enabled: 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_export_enabled: false
bulk_import_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. # F. Write / Delete / Integrity
# enforce_referential_integrity_on_delete: false # -------------------------------------------------------------------------------
# enforce_referential_integrity_on_write: false # allow_cascading_deletes: true
# etag_support_enabled: true # allow_contains_searches: true
# expunge_enabled: true # allow_external_references: true
# client_id_strategy: ALPHANUMERIC # allow_multiple_delete: true
# server_id_strategy: SEQUENTIAL_NUMERIC # allow_override_default_search_params: true
# fhirpath_interceptor_enabled: false # auto_create_placeholder_reference_targets: false
# filter_search_enabled: true # mass_ingestion_mode_enabled: false
# graphql_enabled: true # 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 narrative_enabled: false
# validation:
# requests_enabled: true
# responses_enabled: true
# -------------------------------------------------------------------------------
# H. MDM (Master Data Management)
# -------------------------------------------------------------------------------
mdm_enabled: false mdm_enabled: false
mdm_rules_json_location: "mdm-rules.json" 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
# 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
# -------------------------------------------------------------------------------
# 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: logical_urls:
- http://terminology.hl7.org/* - http://terminology.hl7.org/*
- https://terminology.hl7.org/* - https://terminology.hl7.org/*
@@ -328,38 +312,32 @@ hapi:
- http://loinc.org/* - http://loinc.org/*
- https://loinc.org/* - https://loinc.org/*
### Uncomment the following section, and any sub-properties you need in order to enable # -------------------------------------------------------------------------------
### partitioning support on this server. # J. Partitioning & Multitenancy
#partitioning: # -------------------------------------------------------------------------------
# allow_references_across_partitions: false # partitioning:
# partitioning_include_in_search_hashes: false # allow_references_across_partitions: false
# default_partition_id: 0 # partitioning_include_in_search_hashes: false
### Enable the following setting to enable Database Partitioning Mode # default_partition_id: 0
### See: https://hapifhir.io/hapi-fhir/docs/server_jpa_partitioning/db_partition_mode.html # database_partition_mode_enabled: true
# database_partition_mode_enabled: true # patient_id_partitioning_mode: true
### Partition Style: Partitioning requires a partition interceptor which helps the server # request_tenant_partitioning_mode: false
### select which partition(s) should be accessed for a given request. You can supply your
### own interceptor (see https://hapifhir.io/hapi-fhir/docs/server_jpa_partitioning/partitioning.html#partition-interceptors )
### but the following setting can also be used to use a built-in form.
### Patient ID Partitioning Mode uses the patient/subject ID to determine the partition
# patient_id_partitioning_mode: true
### Request tenant mode can be used for a multi-tenancy setup where the request path is
### expected to have an additional path element, e.g. GET http://example.com/fhir/TENANT-ID/Patient/A
# request_tenant_partitioning_mode: false
# -------------------------------------------------------------------------------
# K. CORS
# -------------------------------------------------------------------------------
cors: cors:
allow_Credentials: true 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: allowed_origin:
- '*' - "*"
# Search coordinator thread pool sizes # -------------------------------------------------------------------------------
# L. Search Orchestration
# -------------------------------------------------------------------------------
search-coord-core-pool-size: 20 search-coord-core-pool-size: 20
search-coord-max-pool-size: 100 search-coord-max-pool-size: 100
search-coord-queue-capacity: 200 search-coord-queue-capacity: 200
# Search Prefetch Thresholds. # Search Prefetch Thresholds.
# This setting sets the number of search results to prefetch. For example, if this list # 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 # 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 # attempt to load more. If the user requests subsequent page(s) of results and goes
@@ -367,87 +345,78 @@ hapi:
# The system will progressively work through these thresholds. # 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 # 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. # 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 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 # comma-separated package names, will be @ComponentScan'ed by Spring to allow for creating custom Spring beans
#custom-bean-packages:
# custom-provider-classes:
# custom-interceptor-classes:
# custom-provider-classes:
# comma-separated list of fully qualified interceptor classes.
# classes listed here will be fetched from the Spring context when combined with 'custom-bean-packages',
# or will be instantiated via reflection using a no-arg constructor; then registered with the server
#custom-interceptor-classes:
# comma-separated list of fully qualified provider classes.
# classes listed here will be fetched from the Spring context when combined with 'custom-bean-packages',
# or will be instantiated via reflection using a no-arg constructor; then registered with the server
#custom-provider-classes:
# specify what should be stored in meta.source based on StoreMetaSourceInformationEnum defaults to NONE
# store_meta_source_information: NONE # store_meta_source_information: NONE
# Threadpool size for BATCH'ed GETs in a bundle. # bundle_batch_pool_size: 10
# bundle_batch_pool_size: 10 # bundle_batch_pool_max_size: 50
# bundle_batch_pool_max_size: 50
# logger: # -------------------------------------------------------------------------------
# error_format: 'ERROR - ${requestVerb} ${requestUrl}' # N. Logging
# format: >- # -------------------------------------------------------------------------------
# Path[${servletPath}] Source[${requestHeader.x-forwarded-for}] # logger:
# Operation[${operationType} ${operationName} ${idOrResourceName}] # error_format: "ERROR - ${requestVerb} ${requestUrl}"
# UA[${requestHeader.user-agent}] Params[${requestParameters}] # format: >-
# ResponseEncoding[${responseEncodingNoDefault}] # Path[${servletPath}] Source[${requestHeader.x-forwarded-for}]
# log_exceptions: true # Operation[${operationType} ${operationName} ${idOrResourceName}]
# name: fhirtest.access # UA[${requestHeader.user-agent}] Params[${requestParameters}]
# max_binary_size: 104857600 # ResponseEncoding[${responseEncodingNoDefault}]
# max_page_size: 200 # log_exceptions: true
# retain_cached_searches_mins: 60 # name: fhirtest.access
# 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. # O. Storage / Pagination / Caching
#remote_terminology_service: # -------------------------------------------------------------------------------
# all: # max_binary_size: 104857600
# system: '*' # max_page_size: 200
# url: 'https://tx.fhir.org/r4/' # retain_cached_searches_mins: 60
# snomed: # reuse_cached_search_results_millis: 60000
# 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
inline_resource_storage_below_size: 4000 inline_resource_storage_below_size: 4000
# bulk_export_enabled: true
# subscription: # -------------------------------------------------------------------------------
# resthook_enabled: true # P. Remote Terminology Service (disabled by default)
# websocket_enabled: false # -------------------------------------------------------------------------------
# polling_interval_ms: 5000 # remote_terminology_service:
# immediately_queued: false # all:
# email: # system: "*"
# from: some@test.com # url: "https://tx.fhir.org/r4/"
# host: google.com # snomed:
# port: # system: "http://snomed.info/sct"
# username: # url: "https://tx.fhir.org/r4/"
# password: # loinc:
# auth: # system: "http://loinc.org"
# startTlsEnable: # url: "https://hapi.fhir.org/baseR4/"
# startTlsRequired:
# quitWait: # -------------------------------------------------------------------------------
# lastn_enabled: true # Q. Subscriptions (disabled by default)
# store_resource_in_lucene_index_enabled: true # -------------------------------------------------------------------------------
### This is configuration for normalized quantity search level default is 0 # subscription:
### 0: NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED - default # resthook_enabled: true
### 1: NORMALIZED_QUANTITY_STORAGE_SUPPORTED # websocket_enabled: false
### 2: NORMALIZED_QUANTITY_SEARCH_SUPPORTED # polling_interval_ms: 5000
# normalized_quantity_search_level: 2 # immediately_queued: false
# email:
# from: some@test.com
# host: google.com
# port:
# username:
# password:
# auth:
# startTlsEnable:
# startTlsRequired:
# quitWait:
# -------------------------------------------------------------------------------
# R. LastN (analytics)
# -------------------------------------------------------------------------------
# lastn_enabled: true

View File

@@ -1,7 +1,7 @@
#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 #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: server:
# servlet: # servlet:
# context-path: /example/path # context-path: /example/path
port: 8080 port: 8080
#Adds the option to go to e.g. http://localhost:8080/actuator/health for seeing the running configuration #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 #see https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints
@@ -53,31 +53,38 @@ spring:
maximum-pool-size: 10 maximum-pool-size: 10
jpa: jpa:
properties: properties:
hibernate.format_sql: false hibernate:
hibernate.show_sql: false format_sql: false
show_sql: false
#Hibernate dialect is automatically detected except Postgres and H2. #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 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 #If using postgres, then supply the value of ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgres94Dialect
hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
# hibernate.hbm2ddl.auto: update # hibernate.hbm2ddl.auto: update
# hibernate.jdbc.batch_size: 20 # hibernate.jdbc.batch_size: 20
# hibernate.cache.use_query_cache: false # hibernate.cache.use_query_cache: false
# hibernate.cache.use_second_level_cache: false # hibernate.cache.use_second_level_cache: false
# hibernate.cache.use_structured_entries: false # hibernate.cache.use_structured_entries: false
# hibernate.cache.use_minimal_puts: false # hibernate.cache.use_minimal_puts: false
### These settings will enable fulltext search with lucene or elastic ### These settings will enable fulltext search with lucene or elastic
hibernate.search.enabled: true
search:
enabled: true
### lucene parameters ### lucene parameters
# hibernate.search.backend.type: lucene # hibernate.search.backend.type: lucene
# hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiLuceneAnalysisConfigurer # hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiLuceneAnalysisConfigurer
# hibernate.search.backend.directory.type: local-filesystem # hibernate.search.backend.directory.type: local-filesystem
# hibernate.search.backend.directory.root: target/lucenefiles # hibernate.search.backend.directory.root: target/lucenefiles
# hibernate.search.backend.lucene_version: lucene_current # hibernate.search.backend.lucene_version: lucene_current
### elastic parameters ===> see also elasticsearch section below <=== ### elastic parameters ===> see also elasticsearch section below <===
# hibernate.search.backend.type: elasticsearch # hibernate.search.backend.type: elasticsearch
# hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer # hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer
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
hapi: hapi:
fhir: fhir:
### This flag when enabled to true, will avail evaluate measure operations from CR Module. ### This flag when enabled to true, will avail evaluate measure operations from CR Module.

View File

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