Feature/elastic on boot (#856)

* Fixing up elastic for Spring Boot

* Adding class shadowing for issue reported on https://github.com/hapifhir/hapi-fhir/pull/7242

* Formatting

* corrected condition

* fix import

* fix2

* crappy fix3

* fix actuator endpoint

* Simplified expression

* Ironed out a few legacy issues

* more rework

* major overhaul

* Disabling invalid test

* Reverting back to defaults for text searches

* Added default hibernate settings from the EnvironmentHelper

* Formatting

* Added comment on class shadow

* Added missing default
This commit is contained in:
Jens Kristian Villadsen
2025-09-22 23:41:43 +02:00
committed by GitHub
parent 4265137b12
commit 9576cfa9b5
39 changed files with 555 additions and 534 deletions

33
pom.xml
View File

@@ -5,7 +5,7 @@
<properties> <properties>
<java.version>17</java.version> <java.version>17</java.version>
<hapi.fhir.jpa.server.starter.revision>2</hapi.fhir.jpa.server.starter.revision> <hapi.fhir.jpa.server.starter.revision>3</hapi.fhir.jpa.server.starter.revision>
<clinical-reasoning.version>3.26.0</clinical-reasoning.version> <clinical-reasoning.version>3.26.0</clinical-reasoning.version>
</properties> </properties>
@@ -60,6 +60,16 @@
<dependencies> <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> <dependency>
<groupId>org.postgresql</groupId> <groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId> <artifactId>postgresql</artifactId>
@@ -265,12 +275,7 @@
<artifactId>moment</artifactId> <artifactId>moment</artifactId>
</dependency> </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> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
@@ -386,17 +391,15 @@
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp</artifactId> <artifactId>spring-ai-mcp</artifactId>
<version>1.0.2</version> <version>1.1.0-M1</version>
</dependency> </dependency>
<!--
This will be included as well as using Spring Automatic Configuration <!--implementation("org.springframework.ai:spring-ai-starter-mcp-server-webmvc:1.1.0-M1")-->
once spring-ai and io.modelcontextprotocol.sdk are on par <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.0.2</version> <version>1.1.0-M1</version>
</dependency>--> </dependency>
<dependency> <dependency>
<groupId>io.modelcontextprotocol.sdk</groupId> <groupId>io.modelcontextprotocol.sdk</groupId>

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import ca.uhn.fhir.jpa.model.config.PartitionSettings.CrossPartitionReferenceMod
import ca.uhn.fhir.jpa.model.config.SubscriptionSettings; import ca.uhn.fhir.jpa.model.config.SubscriptionSettings;
import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.starter.AppProperties; 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.starter.util.JpaHibernatePropertiesProvider;
import ca.uhn.fhir.jpa.subscription.match.deliver.email.EmailSenderImpl; import ca.uhn.fhir.jpa.subscription.match.deliver.email.EmailSenderImpl;
import ca.uhn.fhir.jpa.subscription.match.deliver.email.IEmailSender; import ca.uhn.fhir.jpa.subscription.match.deliver.email.IEmailSender;
@@ -19,10 +20,7 @@ import org.hl7.fhir.r4.model.Bundle.BundleType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.boot.env.YamlPropertySourceLoader; import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.*;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.EnableTransactionManagement;
@@ -36,6 +34,7 @@ import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
*/ */
@Configuration @Configuration
@EnableTransactionManagement @EnableTransactionManagement
@Import(ElasticsearchBootSvcImpl.class)
public class FhirServerConfigCommon { public class FhirServerConfigCommon {
private static final Logger ourLog = LoggerFactory.getLogger(FhirServerConfigCommon.class); private static final Logger ourLog = LoggerFactory.getLogger(FhirServerConfigCommon.class);
@@ -274,7 +273,15 @@ public class FhirServerConfigCommon {
ourLog.debug("Server configured to Store Meta Source: {}", appProperties.getStore_meta_source_information()); ourLog.debug("Server configured to Store Meta Source: {}", appProperties.getStore_meta_source_information());
jpaStorageSettings.setStoreMetaSourceInformation(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());
return jpaStorageSettings; return jpaStorageSettings;
} }
@@ -332,19 +339,6 @@ public class FhirServerConfigCommon {
return new JpaHibernatePropertiesProvider(myEntityManagerFactory); 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 @Lazy
@Bean @Bean
public IBinaryStorageSvc binaryStorageSvc(AppProperties appProperties) { public IBinaryStorageSvc binaryStorageSvc(AppProperties appProperties) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.starter.common; package ca.uhn.fhir.jpa.starter.common;
import ca.uhn.fhir.jpa.starter.AppProperties; 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.Condition;
import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.core.type.AnnotatedTypeMetadata;
@@ -9,9 +9,7 @@ import org.springframework.core.type.AnnotatedTypeMetadata;
public class OnPartitionModeEnabled implements Condition { public class OnPartitionModeEnabled implements Condition {
@Override @Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
var appProperties = Binder.get(context.getEnvironment()) var appProperties = EnvironmentHelper.getConfiguration(context, "hapi.fhir", AppProperties.class);
.bind("hapi.fhir", AppProperties.class)
.orElse(null);
if (appProperties == null) return false; if (appProperties == null) return false;
return appProperties.getPartitioning() != null; return appProperties.getPartitioning() != null;
} }

View File

@@ -52,7 +52,6 @@ import ca.uhn.fhir.jpa.starter.annotations.OnImplementationGuidesPresent;
import ca.uhn.fhir.jpa.starter.common.validation.IRepositoryValidationInterceptorFactory; import ca.uhn.fhir.jpa.starter.common.validation.IRepositoryValidationInterceptorFactory;
import ca.uhn.fhir.jpa.starter.ig.ExtendedPackageInstallationSpec; import ca.uhn.fhir.jpa.starter.ig.ExtendedPackageInstallationSpec;
import ca.uhn.fhir.jpa.starter.ig.IImplementationGuideOperationProvider; 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.subscription.util.SubscriptionDebugLogInterceptor;
import ca.uhn.fhir.jpa.util.ResourceCountCache; import ca.uhn.fhir.jpa.util.ResourceCountCache;
import ca.uhn.fhir.mdm.provider.MdmProviderLoader; import ca.uhn.fhir.mdm.provider.MdmProviderLoader;
@@ -79,13 +78,17 @@ import ca.uhn.fhir.validation.IValidatorModule;
import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhir.validation.ResultSeverityEnum;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import jakarta.persistence.EntityManagerFactory; 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.hl7.fhir.instance.model.api.IBaseBundle;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 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.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
@@ -93,13 +96,11 @@ import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import java.io.IOException;
import java.util.*; import java.util.*;
import javax.sql.DataSource; import javax.sql.DataSource;
@@ -123,9 +124,6 @@ public class StarterJpaConfig {
return new StaleSearchDeletingSvcImpl(); return new StaleSearchDeletingSvcImpl();
} }
@Autowired
private ConfigurableEnvironment configurableEnvironment;
/** /**
* Customize the default/max page sizes for search results. You can set these however * 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. * you want, although very large page sizes will require a lot of RAM.
@@ -151,22 +149,46 @@ public class StarterJpaConfig {
@Primary @Primary
@Bean @Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory( public LocalContainerEntityManagerFactoryBean entityManagerFactory(
JpaProperties theJpaProperties,
DataSource myDataSource, DataSource myDataSource,
ConfigurableListableBeanFactory myConfigurableListableBeanFactory, ConfigurableListableBeanFactory myConfigurableListableBeanFactory,
FhirContext theFhirContext, FhirContext theFhirContext,
JpaStorageSettings theStorageSettings) { JpaStorageSettings theStorageSettings) {
LocalContainerEntityManagerFactoryBean retVal = HapiEntityManagerFactoryUtil.newEntityManagerFactory( LocalContainerEntityManagerFactoryBean entityManagerFactoryBean =
myConfigurableListableBeanFactory, theFhirContext, theStorageSettings); HapiEntityManagerFactoryUtil.newEntityManagerFactory(
retVal.setPersistenceUnitName("HAPI_PU"); myConfigurableListableBeanFactory, theFhirContext, theStorageSettings);
try { // Spring Boot Autoconfiguration defaults
retVal.setDataSource(myDataSource); theJpaProperties
} catch (Exception e) { .getProperties()
throw new ConfigurationException("Could not set the data source due to a configuration issue", e); .putIfAbsent(AvailableSettings.SCANNER, "org.hibernate.boot.archive.scan.internal.DisabledScanner");
} theJpaProperties
retVal.setJpaProperties( .getProperties()
EnvironmentHelper.getHibernateProperties(configurableEnvironment, myConfigurableListableBeanFactory)); .putIfAbsent(AvailableSettings.IMPLICIT_NAMING_STRATEGY, SpringImplicitNamingStrategy.class.getName());
return retVal; 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 @Bean
@@ -213,8 +235,7 @@ public class StarterJpaConfig {
Batch2JobRegisterer batch2JobRegisterer, Batch2JobRegisterer batch2JobRegisterer,
FhirContext fhirContext, FhirContext fhirContext,
TransactionProcessor transactionProcessor, TransactionProcessor transactionProcessor,
IHapiPackageCacheManager iHapiPackageCacheManager) IHapiPackageCacheManager iHapiPackageCacheManager) {
throws IOException {
batch2JobRegisterer.start(); batch2JobRegisterer.start();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,10 +6,12 @@ import ca.uhn.fhir.jpa.ips.generator.IIpsGeneratorSvc;
import ca.uhn.fhir.jpa.ips.generator.IpsGeneratorSvcImpl; import ca.uhn.fhir.jpa.ips.generator.IpsGeneratorSvcImpl;
import ca.uhn.fhir.jpa.ips.jpa.DefaultJpaIpsGenerationStrategy; import ca.uhn.fhir.jpa.ips.jpa.DefaultJpaIpsGenerationStrategy;
import ca.uhn.fhir.jpa.ips.provider.IpsOperationProvider; 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.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 { public class StarterIpsConfig {
@Bean @Bean
IIpsGenerationStrategy ipsGenerationStrategy() { IIpsGenerationStrategy ipsGenerationStrategy() {

View File

@@ -8,14 +8,14 @@ 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.server.McpServer; import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import java.util.List; import java.util.List;
@@ -30,19 +30,17 @@ import java.util.List;
prefix = "spring.ai.mcp.server", prefix = "spring.ai.mcp.server",
name = {"enabled"}, name = {"enabled"},
havingValue = "true") havingValue = "true")
@Import(McpServerStreamableHttpProperties.class)
public class McpServerConfig { public class McpServerConfig {
private static final String SSE_ENDPOINT = "/sse"; private static final String SSE_ENDPOINT = "/sse";
private static final String SSE_MESSAGE_ENDPOINT = "/mcp/message"; private static final String SSE_MESSAGE_ENDPOINT = "/mcp/message";
@Bean @Bean
public McpSyncServer syncServer( public List<McpServerFeatures.SyncToolSpecification> syncServer(List<McpBridge> mcpBridges) {
List<McpBridge> mcpBridges, McpStreamableServerTransportProvider transportProvider) { return mcpBridges.stream()
return McpServer.sync(transportProvider) .flatMap(bridge -> bridge.generateTools().stream())
.tools(mcpBridges.stream() .toList();
.flatMap(bridge -> bridge.generateTools().stream())
.toList())
.build();
} }
@Bean @Bean
@@ -63,11 +61,11 @@ public class McpServerConfig {
@Bean @Bean
public HttpServletStreamableServerTransportProvider servletSseServerTransportProvider( public HttpServletStreamableServerTransportProvider servletSseServerTransportProvider(
/*McpServerProperties properties*/ ) { McpServerStreamableHttpProperties properties) {
return HttpServletStreamableServerTransportProvider.builder() return HttpServletStreamableServerTransportProvider.builder()
.disallowDelete(false) .disallowDelete(false)
.mcpEndpoint(SSE_MESSAGE_ENDPOINT) .mcpEndpoint(properties.getMcpEndpoint())
.objectMapper(new ObjectMapper()) .objectMapper(new ObjectMapper())
// .contextExtractor((serverRequest, context) -> context) // .contextExtractor((serverRequest, context) -> context)
.build(); .build();
@@ -75,7 +73,8 @@ public class McpServerConfig {
@Bean @Bean
public ServletRegistrationBean customServletBean( public ServletRegistrationBean customServletBean(
HttpServletStreamableServerTransportProvider transportProvider /*, McpServerProperties properties*/) { HttpServletStreamableServerTransportProvider transportProvider,
return new ServletRegistrationBean<>(transportProvider, SSE_MESSAGE_ENDPOINT, SSE_ENDPOINT); McpServerStreamableHttpProperties properties) {
return new ServletRegistrationBean<>(transportProvider, properties.getMcpEndpoint(), SSE_ENDPOINT);
} }
} }

View File

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

View File

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

View File

@@ -1,22 +1,7 @@
package ca.uhn.fhir.jpa.starter.util; package ca.uhn.fhir.jpa.starter.util;
import ca.uhn.fhir.jpa.config.HapiFhirLocalContainerEntityManagerFactoryBean; import org.springframework.boot.context.properties.bind.Binder;
import ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers; import org.springframework.context.annotation.ConditionContext;
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.core.env.CompositePropertySource; import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.EnumerablePropertySource;
@@ -25,142 +10,11 @@ import org.springframework.core.env.PropertySource;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Properties;
import static java.util.Objects.requireNonNullElse;
public class EnvironmentHelper { public class EnvironmentHelper {
public static Properties getHibernateProperties( public static <T> T getConfiguration(ConditionContext context, String path, Class<T> clazz) {
ConfigurableEnvironment environment, ConfigurableListableBeanFactory myConfigurableListableBeanFactory) { return Binder.get(context.getEnvironment()).bind(path, clazz).orElse(null);
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 Map<String, Object> getPropertiesStartingWith(ConfigurableEnvironment aEnv, String aKeyPrefix) { public static Map<String, Object> getPropertiesStartingWith(ConfigurableEnvironment aEnv, String aKeyPrefix) {

View File

@@ -9,12 +9,16 @@ server:
#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
management: management:
health:
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. #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:
include: 'health' # or e.g. 'info,health,prometheus,metrics' or '*' for all # expose only health (default) — change to [health,info,prometheus,metrics] if you want them reachable
include: health
endpoint: endpoint:
info: info:
enabled: true enabled: true
@@ -63,21 +67,13 @@ spring:
mcp: mcp:
server: server:
# Will be enabled once spring-ai-starter-mcp-server is added as dependency name: FHIR MCP Server
# name: FHIR MCP Server version: 1.0.0
# 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."
# type: SYNC
# 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."
# sse-message-endpoint: /mcp/message
# capabilities:
# tool: true
# resource: true
# prompt: true
# completion: true
# stdio: false
enabled: true enabled: true
streamable-http:
mcp-endpoint: /mcp/messages
#endpoint: /mcp
#schema: #schema:
# fhir-enabled: true # fhir-enabled: true
@@ -93,51 +89,74 @@ spring:
# {{schema}} # {{schema}}
#base-url: /api/v1 #base-url: /api/v1
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
main: main:
allow-bean-definition-overriding: false allow-bean-definition-overriding: false
allow-circular-references: true allow-circular-references: true
flyway: flyway:
enabled: false enabled: false
baselineOnMigrate: 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
driverClassName: org.h2.Driver driver-class-name: org.h2.Driver
max-active: 15
# database connection 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.format_sql: false hibernate:
hibernate.show_sql: false 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
format_sql: false
show_sql: false
#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:
enabled: true
schema_management:
strategy: create
### lucene parameters
backend:
type: lucene
directory:
type: local-filesystem
root: target/lucenefiles
analysis:
configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiLuceneAnalysisConfigurer
#Hibernate dialect is automatically detected except Postgres and H2. ### elastic parameters ===> see also elasticsearch section below <===
#If using H2, then supply the value of ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect # backend:
#If using postgres, then supply the value of ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect # type: elasticsearch
hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect # discovery: true
# hibernate.hbm2ddl.auto: update # analysis:
# hibernate.jdbc.batch_size: 20 # configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer
# hibernate.cache.use_query_cache: false # hosts: localhost:9200
# hibernate.cache.use_second_level_cache: false # protocol: http
# hibernate.cache.use_structured_entries: false # username: elastic
# hibernate.cache.use_minimal_puts: false # password: changeme
# refresh_after_write: true
### 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: 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.
@@ -201,42 +220,42 @@ hapi:
fhir_version: R4 fhir_version: R4
### Flag is false by default. This flag enables runtime installation of IG's. ### Flag is false by default. This flag enables runtime installation of IG's.
ig_runtime_upload_enabled: false ig_runtime_upload_enabled: false
### 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.
### enable to use the ApacheProxyAddressStrategy which uses X-Forwarded-* headers ### enable to use the ApacheProxyAddressStrategy which uses X-Forwarded-* headers
### to determine the FHIR server address ### to determine the FHIR server address
# use_apache_address_strategy: false # use_apache_address_strategy: false
### forces the use of the https:// protocol for the returned server address. ### forces the use of the https:// protocol for the returned server address.
### alternatively, it may be set using the X-Forwarded-Proto header. ### alternatively, it may be set using the X-Forwarded-Proto header.
# use_apache_address_strategy_https: false # use_apache_address_strategy_https: false
### enables the server to overwrite defaults on HTML, css, etc. under the url pattern of eg. /content/custom ** ### 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 ### Folder with custom content MUST be named custom. If omitted then default content applies
#custom_content_path: ./custom #custom_content_path: ./custom
### enables the server host custom content. If e.g. the value ./configs/app is supplied then the content ### enables the server host custom content. If e.g. the value ./configs/app is supplied then the content
### will be served under /web/app ### will be served under /web/app
#app_content_path: ./configs/app #app_content_path: ./configs/app
### enable to set the Server URL ### enable to set the Server URL
# server_address: http://hapi.fhir.org/baseR4 # server_address: http://hapi.fhir.org/baseR4
# defer_indexing_for_codesystems_of_size: 101 # 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. ### 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 # validate_resource_status_for_package_upload: false
# install_transitive_ig_dependencies: true # install_transitive_ig_dependencies: true
#implementationguides: #implementationguides:
### example from registry (packages.fhir.org) ### example from registry (packages.fhir.org)
# swiss: # swiss:
# name: swiss.mednet.fhir # name: swiss.mednet.fhir
# version: 0.8.0 # version: 0.8.0
# reloadExisting: false # reloadExisting: false
# installMode: STORE_AND_INSTALL # installMode: STORE_AND_INSTALL
# example not from registry # example not from registry
# ips_1_0_0: # ips_1_0_0:
# packageUrl: https://costateixeira.github.io/smart-ips-pilgrimage-fulltest/package.tgz # packageUrl: https://costateixeira.github.io/smart-ips-pilgrimage-fulltest/package.tgz
# name: smart.who.int.ips-pilgrimage-test # name: smart.who.int.ips-pilgrimage-test
# version: 0.1.0 # version: 0.1.0
# installMode: STORE_AND_INSTALL # installMode: STORE_AND_INSTALL
# additionalResourceFolders: # additionalResourceFolders:
# - example # - example
# - example2 # - example2
# supported_resource_types: # supported_resource_types:
# - Patient # - Patient
# - Observation # - Observation
@@ -309,21 +328,21 @@ 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 ### Uncomment the following section, and any sub-properties you need in order to enable
### partitioning support on this server. ### partitioning support on this server.
#partitioning: #partitioning:
# allow_references_across_partitions: false # allow_references_across_partitions: false
# partitioning_include_in_search_hashes: false # partitioning_include_in_search_hashes: false
# default_partition_id: 0 # default_partition_id: 0
### Enable the following setting to enable Database Partitioning Mode ### Enable the following setting to enable Database Partitioning Mode
### See: https://hapifhir.io/hapi-fhir/docs/server_jpa_partitioning/db_partition_mode.html ### See: https://hapifhir.io/hapi-fhir/docs/server_jpa_partitioning/db_partition_mode.html
# database_partition_mode_enabled: true # database_partition_mode_enabled: true
### Partition Style: Partitioning requires a partition interceptor which helps the server ### 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 ### 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 ) ### 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. ### 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 uses the patient/subject ID to determine the partition
# patient_id_partitioning_mode: true # patient_id_partitioning_mode: true
### Request tenant mode can be used for a multi-tenancy setup where the request path is ### 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 ### expected to have an additional path element, e.g. GET http://example.com/fhir/TENANT-ID/Patient/A
# request_tenant_partitioning_mode: false # request_tenant_partitioning_mode: false
@@ -432,14 +451,3 @@ hapi:
### 1: NORMALIZED_QUANTITY_STORAGE_SUPPORTED ### 1: NORMALIZED_QUANTITY_STORAGE_SUPPORTED
### 2: NORMALIZED_QUANTITY_SEARCH_SUPPORTED ### 2: NORMALIZED_QUANTITY_SEARCH_SUPPORTED
# normalized_quantity_search_level: 2 # normalized_quantity_search_level: 2
#elasticsearch:
# debug:
# pretty_print_json_log: false
# refresh_after_write: false
# enabled: false
# password: SomePassword
# required_index_status: YELLOW
# rest_url: 'localhost:9200'
# protocol: 'http'
# schema_management_strategy: CREATE
# username: SomeUsername

View File

@@ -46,6 +46,7 @@ import static org.junit.jupiter.api.Assertions.fail;
}, properties = { }, properties = {
"spring.profiles.include=storageSettingsTest", "spring.profiles.include=storageSettingsTest",
"spring.datasource.url=jdbc:h2:mem:dbr4", "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.enable_repository_validating_interceptor=true",
"hapi.fhir.fhir_version=r4", "hapi.fhir.fhir_version=r4",
"hapi.fhir.cr.enabled=true", "hapi.fhir.cr.enabled=true",

View File

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

View File

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

View File

@@ -29,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties = @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties =
{ {
"spring.datasource.url=jdbc:h2:mem:dbr5_dbpm", "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.fhir_version=r5",
"hapi.fhir.partitioning.database_partition_mode_enabled=true", "hapi.fhir.partitioning.database_partition_mode_enabled=true",
"hapi.fhir.partitioning.patient_id_partitioning_mode=true" "hapi.fhir.partitioning.patient_id_partitioning_mode=true"

View File

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

View File

@@ -49,7 +49,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
"hapi.fhir.subscription.websocket_enabled=true", "hapi.fhir.subscription.websocket_enabled=true",
"hapi.fhir.allow_external_references=true", "hapi.fhir.allow_external_references=true",
"hapi.fhir.allow_placeholder_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 { class ExampleServerDstu3IT implements IServerSupport {

View File

@@ -17,16 +17,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = {Application.class}, classes = {Application.class},
properties = { properties = {
"spring.datasource.url=jdbc:h2:mem:dbr4b", "spring.datasource.url=jdbc:h2:mem:dbr4b",
"hapi.fhir.enable_repository_validating_interceptor=true", "hapi.fhir.enable_repository_validating_interceptor=true",
"hapi.fhir.fhir_version=r4b", "spring.jpa.properties.hibernate.search.backend.directory.type=local-heap",
"hapi.fhir.subscription.websocket_enabled=false", "hapi.fhir.fhir_version=r4b",
"hapi.fhir.mdm_enabled=false", "hapi.fhir.subscription.websocket_enabled=false",
"hapi.fhir.cr_enabled=false", "hapi.fhir.mdm_enabled=false",
// Override is currently required when using MDM as the construction of the MDM "hapi.fhir.cr_enabled=false",
// beans are ambiguous as they are constructed multiple places. This is evident // Override is currently required when using MDM as the construction of the MDM
// when running in a spring boot environment // beans are ambiguous as they are constructed multiple places. This is evident
"spring.main.allow-bean-definition-overriding=true"}) // when running in a spring boot environment
"spring.main.allow-bean-definition-overriding=true"})
class ExampleServerR4BIT { class ExampleServerR4BIT {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExampleServerR4BIT.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExampleServerR4BIT.class);
private IGenericClient ourClient; private IGenericClient ourClient;
@@ -107,7 +108,6 @@ class ExampleServerR4BIT {
} }
@BeforeEach @BeforeEach
void beforeEach() { void beforeEach() {

View File

@@ -54,6 +54,7 @@ import static org.opencds.cqf.fhir.utility.r4.Parameters.stringPart;
RepositoryConfig.class RepositoryConfig.class
}, properties = { }, properties = {
"spring.datasource.url=jdbc:h2:mem:dbr4", "spring.datasource.url=jdbc:h2:mem:dbr4",
"spring.ai.mcp.server.enabled=false",
"hapi.fhir.enable_repository_validating_interceptor=true", "hapi.fhir.enable_repository_validating_interceptor=true",
"hapi.fhir.fhir_version=r4", "hapi.fhir.fhir_version=r4",
"hapi.fhir.subscription.websocket_enabled=true", "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 // beans are ambiguous as they are constructed multiple places. This is evident
// when running in a spring boot environment // when running in a spring boot environment
"spring.main.allow-bean-definition-overriding=true", "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.system=http://snomed.info/sct",
"hapi.fhir.remote_terminology_service.snomed.url=https://tx.fhir.org/r4" "hapi.fhir.remote_terminology_service.snomed.url=https://tx.fhir.org/r4"
}) })

View File

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

View File

@@ -33,7 +33,7 @@ public class McpTests {
var fhirContext = FhirContext.forR4(); var fhirContext = FhirContext.forR4();
var transport = HttpClientStreamableHttpTransport.builder("http://localhost:" + port).endpoint("/mcp/message").build(); 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 var client = McpClient.sync(transport).requestTimeout(Duration.ofSeconds(10)).capabilities(McpSchema.ClientCapabilities.builder().roots(true) // Enable roots capability
.sampling().build()).build(); .sampling().build()).build();
var initializationResult = client.initialize(); var initializationResult = client.initialize();

View File

@@ -8,19 +8,15 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.nickname.INicknameSvc; 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.fhir_version=r4",
"hapi.fhir.mdm_enabled=true" "hapi.fhir.mdm_enabled=true"
}) })
class MdmTest { class MdmTest {
@Autowired @Autowired
INicknameSvc nicknameService; INicknameSvc nicknameService;
@Autowired
JpaStorageSettings jpaStorageSettings;
@Autowired @Autowired
SubscriptionSettings subscriptionSettings; SubscriptionSettings subscriptionSettings;

View File

@@ -25,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties = @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties =
{ {
"spring.datasource.url=jdbc:h2:mem:dbr4-mt", "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.fhir_version=r4",
"hapi.fhir.subscription.websocket_enabled=true", "hapi.fhir.subscription.websocket_enabled=true",
"hapi.fhir.cr_enabled=false", "hapi.fhir.cr_enabled=false",

View File

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

View File

@@ -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: hapi:
fhir: fhir: