diff --git a/pom.xml b/pom.xml
index 3373897f60f..ba38a3b8acb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
17
- 2
+ 3
3.26.0
@@ -60,6 +60,16 @@
+
+ org.springframework.boot
+ spring-boot-starter-data-elasticsearch
+ ${spring_boot_version}
+
+
+ co.elastic.clients
+ elasticsearch-java
+
+
org.postgresql
postgresql
@@ -265,12 +275,7 @@
moment
-
-
- co.elastic.clients
- elasticsearch-java
- test
-
+
ca.uhn.hapi.fhir
@@ -386,17 +391,15 @@
org.springframework.ai
spring-ai-mcp
- 1.0.2
+ 1.1.0-M1
-
-
+
org.springframework.ai
spring-ai-starter-mcp-server
- 1.0.2
- -->
+ 1.1.0-M1
+
io.modelcontextprotocol.sdk
diff --git a/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderObservation.java b/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderObservation.java
new file mode 100644
index 00000000000..dd026cf65b6
--- /dev/null
+++ b/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderObservation.java
@@ -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 extends BaseJpaResourceProvider {
+
+ /**
+ * 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 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 theMax,
+ @RawParam Map> 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);
+ }
+ }
+}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/Application.java b/src/main/java/ca/uhn/fhir/jpa/starter/Application.java
index c3033ad605e..4773cd1e982 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/Application.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/Application.java
@@ -16,7 +16,6 @@
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
@@ -26,7 +25,7 @@
import org.springframework.context.annotation.Import;
@ServletComponentScan(basePackageClasses = {RestfulServer.class})
-@SpringBootApplication(exclude = {ElasticsearchRestClientAutoConfiguration.class, ThymeleafAutoConfiguration.class})
+@SpringBootApplication(exclude = {ThymeleafAutoConfiguration.class})
@Import({
StarterCrR4Config.class,
StarterCrDstu3Config.class,
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/annotations/OnImplementationGuidesPresent.java b/src/main/java/ca/uhn/fhir/jpa/starter/annotations/OnImplementationGuidesPresent.java
index c3208014a25..88d290aadb5 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/annotations/OnImplementationGuidesPresent.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/annotations/OnImplementationGuidesPresent.java
@@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.starter.annotations;
import ca.uhn.fhir.jpa.starter.AppProperties;
-import org.springframework.boot.context.properties.bind.Binder;
+import ca.uhn.fhir.jpa.starter.util.EnvironmentHelper;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
@@ -10,9 +10,7 @@ public class OnImplementationGuidesPresent implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) {
- AppProperties config = Binder.get(conditionContext.getEnvironment())
- .bind("hapi.fhir", AppProperties.class)
- .orElse(null);
+ AppProperties config = EnvironmentHelper.getConfiguration(conditionContext, "hapi.fhir", AppProperties.class);
if (config == null) return false;
if (config.getImplementationGuides() == null) return false;
return !config.getImplementationGuides().isEmpty();
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/ElasticsearchConfig.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/ElasticsearchConfig.java
deleted file mode 100644
index df8ce4973f5..00000000000
--- a/src/main/java/ca/uhn/fhir/jpa/starter/common/ElasticsearchConfig.java
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigCommon.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigCommon.java
index 924bc046933..f28a2c6c36b 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigCommon.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigCommon.java
@@ -9,6 +9,7 @@
import ca.uhn.fhir.jpa.model.config.SubscriptionSettings;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.starter.AppProperties;
+import ca.uhn.fhir.jpa.starter.elastic.ElasticsearchBootSvcImpl;
import ca.uhn.fhir.jpa.starter.util.JpaHibernatePropertiesProvider;
import ca.uhn.fhir.jpa.subscription.match.deliver.email.EmailSenderImpl;
import ca.uhn.fhir.jpa.subscription.match.deliver.email.IEmailSender;
@@ -19,10 +20,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.env.YamlPropertySourceLoader;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Lazy;
-import org.springframework.context.annotation.Primary;
+import org.springframework.context.annotation.*;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@@ -36,6 +34,7 @@
*/
@Configuration
@EnableTransactionManagement
+@Import(ElasticsearchBootSvcImpl.class)
public class FhirServerConfigCommon {
private static final Logger ourLog = LoggerFactory.getLogger(FhirServerConfigCommon.class);
@@ -274,7 +273,15 @@ public JpaStorageSettings jpaStorageSettings(AppProperties appProperties) {
ourLog.debug("Server configured to Store Meta Source: {}", appProperties.getStore_meta_source_information());
jpaStorageSettings.setStoreMetaSourceInformation(appProperties.getStore_meta_source_information());
- storageSettings(appProperties, jpaStorageSettings);
+ jpaStorageSettings.setAllowContainsSearches(appProperties.getAllow_contains_searches());
+ jpaStorageSettings.setAllowExternalReferences(appProperties.getAllow_external_references());
+ jpaStorageSettings.setDefaultSearchParamsCanBeOverridden(
+ appProperties.getAllow_override_default_search_params());
+
+ jpaStorageSettings.setNormalizedQuantitySearchLevel(appProperties.getNormalized_quantity_search_level());
+
+ jpaStorageSettings.setIndexOnContainedResources(appProperties.getEnable_index_contained_resource());
+ jpaStorageSettings.setIndexIdentifierOfType(appProperties.getEnable_index_of_type());
return jpaStorageSettings;
}
@@ -332,19 +339,6 @@ public HibernatePropertiesProvider jpaStarterDialectProvider(
return new JpaHibernatePropertiesProvider(myEntityManagerFactory);
}
- protected StorageSettings storageSettings(AppProperties appProperties, JpaStorageSettings jpaStorageSettings) {
- jpaStorageSettings.setAllowContainsSearches(appProperties.getAllow_contains_searches());
- jpaStorageSettings.setAllowExternalReferences(appProperties.getAllow_external_references());
- jpaStorageSettings.setDefaultSearchParamsCanBeOverridden(
- appProperties.getAllow_override_default_search_params());
-
- jpaStorageSettings.setNormalizedQuantitySearchLevel(appProperties.getNormalized_quantity_search_level());
-
- jpaStorageSettings.setIndexOnContainedResources(appProperties.getEnable_index_contained_resource());
- jpaStorageSettings.setIndexIdentifierOfType(appProperties.getEnable_index_of_type());
- return jpaStorageSettings;
- }
-
@Lazy
@Bean
public IBinaryStorageSvc binaryStorageSvc(AppProperties appProperties) {
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigDstu3.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigDstu3.java
index a029d1110fe..15e0272f592 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigDstu3.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigDstu3.java
@@ -9,5 +9,5 @@
@Configuration
@Conditional(OnDSTU3Condition.class)
-@Import({JpaDstu3Config.class, StarterJpaConfig.class, StarterCrDstu3Config.class, ElasticsearchConfig.class})
+@Import({JpaDstu3Config.class, StarterJpaConfig.class, StarterCrDstu3Config.class})
public class FhirServerConfigDstu3 {}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4.java
index 55dce56b2d3..d7490380197 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4.java
@@ -10,11 +10,5 @@
@Configuration
@Conditional(OnR4Condition.class)
-@Import({
- JpaR4Config.class,
- StarterJpaConfig.class,
- StarterCrR4Config.class,
- ElasticsearchConfig.class,
- StarterIpsConfig.class
-})
+@Import({JpaR4Config.class, StarterJpaConfig.class, StarterCrR4Config.class, StarterIpsConfig.class})
public class FhirServerConfigR4 {}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4B.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4B.java
index ab267de02c6..dcd5ab31db9 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4B.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4B.java
@@ -9,5 +9,5 @@
@Configuration
@Conditional(OnR4BCondition.class)
-@Import({JpaR4BConfig.class, SubscriptionTopicConfig.class, StarterJpaConfig.class, ElasticsearchConfig.class})
+@Import({JpaR4BConfig.class, SubscriptionTopicConfig.class, StarterJpaConfig.class})
public class FhirServerConfigR4B {}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR5.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR5.java
index 0aaa6502fa1..7a592853847 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR5.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR5.java
@@ -9,5 +9,5 @@
@Configuration
@Conditional(OnR5Condition.class)
-@Import({StarterJpaConfig.class, JpaR5Config.class, SubscriptionTopicConfig.class, ElasticsearchConfig.class})
+@Import({StarterJpaConfig.class, JpaR5Config.class, SubscriptionTopicConfig.class})
public class FhirServerConfigR5 {}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/OnPartitionModeEnabled.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/OnPartitionModeEnabled.java
index b695babe544..aaf73d11e66 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/common/OnPartitionModeEnabled.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/common/OnPartitionModeEnabled.java
@@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.starter.common;
import ca.uhn.fhir.jpa.starter.AppProperties;
-import org.springframework.boot.context.properties.bind.Binder;
+import ca.uhn.fhir.jpa.starter.util.EnvironmentHelper;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
@@ -9,9 +9,7 @@
public class OnPartitionModeEnabled implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
- var appProperties = Binder.get(context.getEnvironment())
- .bind("hapi.fhir", AppProperties.class)
- .orElse(null);
+ var appProperties = EnvironmentHelper.getConfiguration(context, "hapi.fhir", AppProperties.class);
if (appProperties == null) return false;
return appProperties.getPartitioning() != null;
}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java
index dc717335abb..1c1d2c72535 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java
@@ -52,7 +52,6 @@
import ca.uhn.fhir.jpa.starter.common.validation.IRepositoryValidationInterceptorFactory;
import ca.uhn.fhir.jpa.starter.ig.ExtendedPackageInstallationSpec;
import ca.uhn.fhir.jpa.starter.ig.IImplementationGuideOperationProvider;
-import ca.uhn.fhir.jpa.starter.util.EnvironmentHelper;
import ca.uhn.fhir.jpa.subscription.util.SubscriptionDebugLogInterceptor;
import ca.uhn.fhir.jpa.util.ResourceCountCache;
import ca.uhn.fhir.mdm.provider.MdmProviderLoader;
@@ -79,13 +78,17 @@
import ca.uhn.fhir.validation.ResultSeverityEnum;
import com.google.common.base.Strings;
import jakarta.persistence.EntityManagerFactory;
+import org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy;
+import org.hibernate.cfg.AvailableSettings;
+import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
+import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
@@ -93,13 +96,11 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
-import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.http.HttpHeaders;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.web.cors.CorsConfiguration;
-import java.io.IOException;
import java.util.*;
import javax.sql.DataSource;
@@ -123,9 +124,6 @@ public IStaleSearchDeletingSvc staleSearchDeletingSvc() {
return new StaleSearchDeletingSvcImpl();
}
- @Autowired
- private ConfigurableEnvironment configurableEnvironment;
-
/**
* Customize the default/max page sizes for search results. You can set these however
* you want, although very large page sizes will require a lot of RAM.
@@ -151,22 +149,46 @@ public ResourceCountCache resourceCountsCache(IFhirSystemDao, ?> theSystemDao)
@Primary
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
+ JpaProperties theJpaProperties,
DataSource myDataSource,
ConfigurableListableBeanFactory myConfigurableListableBeanFactory,
FhirContext theFhirContext,
JpaStorageSettings theStorageSettings) {
- LocalContainerEntityManagerFactoryBean retVal = HapiEntityManagerFactoryUtil.newEntityManagerFactory(
- myConfigurableListableBeanFactory, theFhirContext, theStorageSettings);
- retVal.setPersistenceUnitName("HAPI_PU");
-
- try {
- retVal.setDataSource(myDataSource);
- } catch (Exception e) {
- throw new ConfigurationException("Could not set the data source due to a configuration issue", e);
- }
- retVal.setJpaProperties(
- EnvironmentHelper.getHibernateProperties(configurableEnvironment, myConfigurableListableBeanFactory));
- return retVal;
+ LocalContainerEntityManagerFactoryBean entityManagerFactoryBean =
+ HapiEntityManagerFactoryUtil.newEntityManagerFactory(
+ myConfigurableListableBeanFactory, theFhirContext, theStorageSettings);
+
+ // Spring Boot Autoconfiguration defaults
+ theJpaProperties
+ .getProperties()
+ .putIfAbsent(AvailableSettings.SCANNER, "org.hibernate.boot.archive.scan.internal.DisabledScanner");
+ theJpaProperties
+ .getProperties()
+ .putIfAbsent(AvailableSettings.IMPLICIT_NAMING_STRATEGY, SpringImplicitNamingStrategy.class.getName());
+ theJpaProperties
+ .getProperties()
+ .putIfAbsent(
+ AvailableSettings.PHYSICAL_NAMING_STRATEGY,
+ CamelCaseToUnderscoresNamingStrategy.class.getName());
+
+ // Hibernate Search defaults
+ theJpaProperties.getProperties().putIfAbsent(AvailableSettings.FORMAT_SQL, "false");
+ theJpaProperties.getProperties().putIfAbsent(AvailableSettings.SHOW_SQL, "false");
+ theJpaProperties.getProperties().putIfAbsent(AvailableSettings.HBM2DDL_AUTO, "update");
+ theJpaProperties.getProperties().putIfAbsent(AvailableSettings.STATEMENT_BATCH_SIZE, "20");
+ theJpaProperties.getProperties().putIfAbsent(AvailableSettings.USE_QUERY_CACHE, "false");
+ theJpaProperties.getProperties().putIfAbsent(AvailableSettings.USE_SECOND_LEVEL_CACHE, "false");
+ theJpaProperties.getProperties().putIfAbsent(AvailableSettings.USE_STRUCTURED_CACHE, "false");
+ theJpaProperties.getProperties().putIfAbsent(AvailableSettings.USE_MINIMAL_PUTS, "false");
+
+ // Hibernate Search defaults
+ theJpaProperties.getProperties().putIfAbsent(HibernateOrmMapperSettings.ENABLED, "false");
+
+ entityManagerFactoryBean.setPersistenceUnitName("HAPI_PU");
+ entityManagerFactoryBean.setJpaPropertyMap(theJpaProperties.getProperties());
+ entityManagerFactoryBean.setDataSource(myDataSource);
+
+ return entityManagerFactoryBean;
}
@Bean
@@ -213,8 +235,7 @@ public IPackageInstallerSvc packageInstaller(
Batch2JobRegisterer batch2JobRegisterer,
FhirContext fhirContext,
TransactionProcessor transactionProcessor,
- IHapiPackageCacheManager iHapiPackageCacheManager)
- throws IOException {
+ IHapiPackageCacheManager iHapiPackageCacheManager) {
batch2JobRegisterer.start();
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/validation/OnRemoteTerminologyPresent.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/validation/OnRemoteTerminologyPresent.java
index bd11463a3ea..8459323df5b 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/common/validation/OnRemoteTerminologyPresent.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/common/validation/OnRemoteTerminologyPresent.java
@@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.starter.common.validation;
import ca.uhn.fhir.jpa.starter.AppProperties;
-import org.springframework.boot.context.properties.bind.Binder;
+import ca.uhn.fhir.jpa.starter.util.EnvironmentHelper;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
@@ -10,9 +10,8 @@ public class OnRemoteTerminologyPresent implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) {
- AppProperties config = Binder.get(conditionContext.getEnvironment())
- .bind("hapi.fhir", AppProperties.class)
- .orElse(null);
+ AppProperties config = EnvironmentHelper.getConfiguration(conditionContext, "hapi.fhir", AppProperties.class);
+
if (config == null) return false;
if (config.getRemoteTerminologyServicesMap() == null) return false;
return !config.getRemoteTerminologyServicesMap().isEmpty();
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/elastic/ElasticConfigCondition.java b/src/main/java/ca/uhn/fhir/jpa/starter/elastic/ElasticConfigCondition.java
new file mode 100644
index 00000000000..b98f194fd51
--- /dev/null
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/elastic/ElasticConfigCondition.java
@@ -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;
+ }
+}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/elastic/ElasticsearchBootSvcImpl.java b/src/main/java/ca/uhn/fhir/jpa/starter/elastic/ElasticsearchBootSvcImpl.java
new file mode 100644
index 00000000000..82d8511330d
--- /dev/null
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/elastic/ElasticsearchBootSvcImpl.java
@@ -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 getObservationResources(Collection extends IResourcePersistentId> thePids) {
+ SearchRequest searchRequest = buildObservationResourceSearchRequest(thePids);
+ try {
+ SearchResponse observationDocumentResponse =
+ myRestHighLevelClient.search(searchRequest, ObservationJson.class);
+ List> 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 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));
+ }
+}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/ig/IgConfigCondition.java b/src/main/java/ca/uhn/fhir/jpa/starter/ig/IgConfigCondition.java
deleted file mode 100644
index a93736b0cd2..00000000000
--- a/src/main/java/ca/uhn/fhir/jpa/starter/ig/IgConfigCondition.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR4OperationProvider.java b/src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR4OperationProvider.java
index 8ffb62c4ee0..2b2519e9185 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR4OperationProvider.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR4OperationProvider.java
@@ -7,16 +7,18 @@
import ca.uhn.fhir.rest.annotation.OperationParam;
import org.hl7.fhir.r4.model.Base64BinaryType;
import org.hl7.fhir.r4.model.Parameters;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Service;
import java.io.IOException;
-@Conditional({OnR4Condition.class, IgConfigCondition.class})
+@Conditional({OnR4Condition.class})
+@ConditionalOnProperty(name = "hapi.fhir.ig_runtime_upload_enabled", havingValue = "true")
@Service
public class ImplementationGuideR4OperationProvider implements IImplementationGuideOperationProvider {
- IPackageInstallerSvc packageInstallerSvc;
+ final IPackageInstallerSvc packageInstallerSvc;
public ImplementationGuideR4OperationProvider(IPackageInstallerSvc packageInstallerSvc) {
this.packageInstallerSvc = packageInstallerSvc;
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR5OperationProvider.java b/src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR5OperationProvider.java
index b045a365897..b8453410550 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR5OperationProvider.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR5OperationProvider.java
@@ -7,16 +7,18 @@
import ca.uhn.fhir.rest.annotation.OperationParam;
import org.hl7.fhir.r5.model.Base64BinaryType;
import org.hl7.fhir.r5.model.Parameters;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Service;
import java.io.IOException;
-@Conditional({OnR5Condition.class, IgConfigCondition.class})
+@Conditional({OnR5Condition.class})
+@ConditionalOnProperty(name = "hapi.fhir.ig_runtime_upload_enabled", havingValue = "true")
@Service
public class ImplementationGuideR5OperationProvider implements IImplementationGuideOperationProvider {
- IPackageInstallerSvc packageInstallerSvc;
+ final IPackageInstallerSvc packageInstallerSvc;
public ImplementationGuideR5OperationProvider(IPackageInstallerSvc packageInstallerSvc) {
this.packageInstallerSvc = packageInstallerSvc;
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/ips/IpsConfigCondition.java b/src/main/java/ca/uhn/fhir/jpa/starter/ips/IpsConfigCondition.java
deleted file mode 100644
index ca26e48ff7f..00000000000
--- a/src/main/java/ca/uhn/fhir/jpa/starter/ips/IpsConfigCondition.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/ips/StarterIpsConfig.java b/src/main/java/ca/uhn/fhir/jpa/starter/ips/StarterIpsConfig.java
index 54e75f8c9f2..ffc176089c8 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/ips/StarterIpsConfig.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/ips/StarterIpsConfig.java
@@ -6,10 +6,12 @@
import ca.uhn.fhir.jpa.ips.generator.IpsGeneratorSvcImpl;
import ca.uhn.fhir.jpa.ips.jpa.DefaultJpaIpsGenerationStrategy;
import ca.uhn.fhir.jpa.ips.provider.IpsOperationProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Configuration;
-@Conditional(IpsConfigCondition.class)
+@Configuration
+@ConditionalOnProperty(name = "hapi.fhir.ips_enabled", havingValue = "true")
public class StarterIpsConfig {
@Bean
IIpsGenerationStrategy ipsGenerationStrategy() {
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/mcp/McpServerConfig.java b/src/main/java/ca/uhn/fhir/jpa/starter/mcp/McpServerConfig.java
index 9dec687d51f..1254f437fb2 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/mcp/McpServerConfig.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/mcp/McpServerConfig.java
@@ -8,14 +8,14 @@
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceRegistry;
import ca.uhn.hapi.fhir.cdshooks.module.CdsHooksObjectMapperFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
-import io.modelcontextprotocol.server.McpServer;
-import io.modelcontextprotocol.server.McpSyncServer;
+import io.modelcontextprotocol.server.McpServerFeatures;
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.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
import java.util.List;
@@ -30,19 +30,17 @@
prefix = "spring.ai.mcp.server",
name = {"enabled"},
havingValue = "true")
+@Import(McpServerStreamableHttpProperties.class)
public class McpServerConfig {
private static final String SSE_ENDPOINT = "/sse";
private static final String SSE_MESSAGE_ENDPOINT = "/mcp/message";
@Bean
- public McpSyncServer syncServer(
- List mcpBridges, McpStreamableServerTransportProvider transportProvider) {
- return McpServer.sync(transportProvider)
- .tools(mcpBridges.stream()
- .flatMap(bridge -> bridge.generateTools().stream())
- .toList())
- .build();
+ public List syncServer(List mcpBridges) {
+ return mcpBridges.stream()
+ .flatMap(bridge -> bridge.generateTools().stream())
+ .toList();
}
@Bean
@@ -63,11 +61,11 @@ public McpCdsBridge mcpCdsBridge(FhirContext fhirContext, ICdsServiceRegistry cd
@Bean
public HttpServletStreamableServerTransportProvider servletSseServerTransportProvider(
- /*McpServerProperties properties*/ ) {
+ McpServerStreamableHttpProperties properties) {
return HttpServletStreamableServerTransportProvider.builder()
.disallowDelete(false)
- .mcpEndpoint(SSE_MESSAGE_ENDPOINT)
+ .mcpEndpoint(properties.getMcpEndpoint())
.objectMapper(new ObjectMapper())
// .contextExtractor((serverRequest, context) -> context)
.build();
@@ -75,7 +73,8 @@ public HttpServletStreamableServerTransportProvider servletSseServerTransportPro
@Bean
public ServletRegistrationBean customServletBean(
- HttpServletStreamableServerTransportProvider transportProvider /*, McpServerProperties properties*/) {
- return new ServletRegistrationBean<>(transportProvider, SSE_MESSAGE_ENDPOINT, SSE_ENDPOINT);
+ HttpServletStreamableServerTransportProvider transportProvider,
+ McpServerStreamableHttpProperties properties) {
+ return new ServletRegistrationBean<>(transportProvider, properties.getMcpEndpoint(), SSE_ENDPOINT);
}
}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/mdm/MdmConfig.java b/src/main/java/ca/uhn/fhir/jpa/starter/mdm/MdmConfig.java
index 2e8a1f16106..604b97fb420 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/mdm/MdmConfig.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/mdm/MdmConfig.java
@@ -9,8 +9,8 @@
import ca.uhn.fhir.mdm.rules.config.MdmSettings;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.DefaultResourceLoader;
@@ -20,7 +20,7 @@
import java.nio.charset.StandardCharsets;
@Configuration
-@Conditional(MdmConfigCondition.class)
+@ConditionalOnProperty(prefix = "hapi.fhir", name = "mdm_enabled")
@Import({MdmConsumerConfig.class, MdmSubmitterConfig.class, NicknameServiceConfig.class})
public class MdmConfig {
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/mdm/MdmConfigCondition.java b/src/main/java/ca/uhn/fhir/jpa/starter/mdm/MdmConfigCondition.java
deleted file mode 100644
index 7c3bf5b47d6..00000000000
--- a/src/main/java/ca/uhn/fhir/jpa/starter/mdm/MdmConfigCondition.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/util/EnvironmentHelper.java b/src/main/java/ca/uhn/fhir/jpa/starter/util/EnvironmentHelper.java
index 5d983ec54a5..6499b5147fd 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/util/EnvironmentHelper.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/util/EnvironmentHelper.java
@@ -1,22 +1,7 @@
package ca.uhn.fhir.jpa.starter.util;
-import ca.uhn.fhir.jpa.config.HapiFhirLocalContainerEntityManagerFactoryBean;
-import ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers;
-import ca.uhn.fhir.jpa.search.elastic.ElasticsearchHibernatePropertiesBuilder;
-import org.apache.lucene.util.Version;
-import org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy;
-import org.hibernate.cfg.AvailableSettings;
-import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings;
-import org.hibernate.search.backend.elasticsearch.index.IndexStatus;
-import org.hibernate.search.backend.lucene.cfg.LuceneBackendSettings;
-import org.hibernate.search.backend.lucene.cfg.LuceneIndexSettings;
-import org.hibernate.search.backend.lucene.lowlevel.directory.impl.LocalFileSystemDirectoryProvider;
-import org.hibernate.search.engine.cfg.BackendSettings;
-import org.hibernate.search.mapper.orm.automaticindexing.session.AutomaticIndexingSynchronizationStrategyNames;
-import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
-import org.hibernate.search.mapper.orm.schema.management.SchemaManagementStrategyName;
-import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
-import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
@@ -25,142 +10,11 @@
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
-import java.util.Properties;
-
-import static java.util.Objects.requireNonNullElse;
public class EnvironmentHelper {
- public static Properties getHibernateProperties(
- ConfigurableEnvironment environment, ConfigurableListableBeanFactory myConfigurableListableBeanFactory) {
- Properties properties = new Properties();
- Map jpaProps = getPropertiesStartingWith(environment, "spring.jpa.properties");
- for (Map.Entry 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 jpaPropsEnv = getPropertiesStartingWith(environment, "SPRING_JPA_PROPERTIES");
- for (Map.Entry 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 hapiJpaPropertyMap = new HapiFhirLocalContainerEntityManagerFactoryBean(
- myConfigurableListableBeanFactory)
- .getJpaPropertyMap();
- hapiJpaPropertyMap.forEach(properties::putIfAbsent);
-
- // hapi-fhir-jpaserver-starter defaults
- properties.putIfAbsent(AvailableSettings.FORMAT_SQL, false);
- properties.putIfAbsent(AvailableSettings.SHOW_SQL, false);
- properties.putIfAbsent(AvailableSettings.HBM2DDL_AUTO, "update");
- properties.putIfAbsent(AvailableSettings.STATEMENT_BATCH_SIZE, 20);
- properties.putIfAbsent(AvailableSettings.USE_QUERY_CACHE, false);
- properties.putIfAbsent(AvailableSettings.USE_SECOND_LEVEL_CACHE, false);
- properties.putIfAbsent(AvailableSettings.USE_STRUCTURED_CACHE, false);
- properties.putIfAbsent(AvailableSettings.USE_MINIMAL_PUTS, false);
-
- // Hibernate Search defaults
- properties.putIfAbsent(HibernateOrmMapperSettings.ENABLED, false);
- if (Boolean.parseBoolean(String.valueOf(properties.get(HibernateOrmMapperSettings.ENABLED)))) {
- if (isElasticsearchEnabled(environment)) {
- properties.putIfAbsent(
- BackendSettings.backendKey(BackendSettings.TYPE), ElasticsearchBackendSettings.TYPE_NAME);
- } else {
- properties.putIfAbsent(
- BackendSettings.backendKey(BackendSettings.TYPE), LuceneBackendSettings.TYPE_NAME);
- }
-
- if (properties
- .get(BackendSettings.backendKey(BackendSettings.TYPE))
- .equals(LuceneBackendSettings.TYPE_NAME)) {
- properties.putIfAbsent(
- BackendSettings.backendKey(LuceneIndexSettings.DIRECTORY_TYPE),
- LocalFileSystemDirectoryProvider.NAME);
- properties.putIfAbsent(
- BackendSettings.backendKey(LuceneIndexSettings.DIRECTORY_ROOT), "target/lucenefiles");
- properties.putIfAbsent(
- BackendSettings.backendKey(LuceneBackendSettings.ANALYSIS_CONFIGURER),
- HapiHSearchAnalysisConfigurers.HapiLuceneAnalysisConfigurer.class.getName());
- properties.putIfAbsent(
- BackendSettings.backendKey(LuceneBackendSettings.LUCENE_VERSION), Version.LATEST);
-
- } else if (properties
- .get(BackendSettings.backendKey(BackendSettings.TYPE))
- .equals(ElasticsearchBackendSettings.TYPE_NAME)) {
- ElasticsearchHibernatePropertiesBuilder builder = new ElasticsearchHibernatePropertiesBuilder();
- IndexStatus requiredIndexStatus =
- environment.getProperty("elasticsearch.required_index_status", IndexStatus.class);
- builder.setRequiredIndexStatus(requireNonNullElse(requiredIndexStatus, IndexStatus.YELLOW));
- builder.setHosts(getElasticsearchServerUrl(environment));
- builder.setUsername(getElasticsearchServerUsername(environment));
- builder.setPassword(getElasticsearchServerPassword(environment));
- builder.setProtocol(getElasticsearchServerProtocol(environment));
- SchemaManagementStrategyName indexSchemaManagementStrategy = environment.getProperty(
- "elasticsearch.schema_management_strategy", SchemaManagementStrategyName.class);
- builder.setIndexSchemaManagementStrategy(
- requireNonNullElse(indexSchemaManagementStrategy, SchemaManagementStrategyName.CREATE));
- Boolean refreshAfterWrite =
- environment.getProperty("elasticsearch.debug.refresh_after_write", Boolean.class);
- if (refreshAfterWrite == null || !refreshAfterWrite) {
- builder.setDebugIndexSyncStrategy(AutomaticIndexingSynchronizationStrategyNames.ASYNC);
- } else {
- builder.setDebugIndexSyncStrategy(AutomaticIndexingSynchronizationStrategyNames.READ_SYNC);
- }
- builder.setDebugPrettyPrintJsonLog(requireNonNullElse(
- environment.getProperty("elasticsearch.debug.pretty_print_json_log", Boolean.class), false));
- builder.apply(properties);
-
- } else {
- throw new UnsupportedOperationException("Unsupported Hibernate Search backend: "
- + properties.get(BackendSettings.backendKey(BackendSettings.TYPE)));
- }
- }
-
- return properties;
- }
-
- public static String getElasticsearchServerUrl(ConfigurableEnvironment environment) {
- return environment.getProperty("elasticsearch.rest_url", String.class);
- }
-
- public static String getElasticsearchServerProtocol(ConfigurableEnvironment environment) {
- return environment.getProperty("elasticsearch.protocol", String.class, "http");
- }
-
- public static String getElasticsearchServerUsername(ConfigurableEnvironment environment) {
- return environment.getProperty("elasticsearch.username");
- }
-
- public static String getElasticsearchServerPassword(ConfigurableEnvironment environment) {
- return environment.getProperty("elasticsearch.password");
- }
-
- public static Boolean isElasticsearchEnabled(ConfigurableEnvironment environment) {
- if (environment.getProperty("elasticsearch.enabled", Boolean.class) != null) {
- return environment.getProperty("elasticsearch.enabled", Boolean.class);
- } else {
- return false;
- }
+ public static T getConfiguration(ConditionContext context, String path, Class clazz) {
+ return Binder.get(context.getEnvironment()).bind(path, clazz).orElse(null);
}
public static Map getPropertiesStartingWith(ConfigurableEnvironment aEnv, String aKeyPrefix) {
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 662608a907a..3a9ac8a83e3 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -9,12 +9,16 @@ server:
#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:
+ 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.
endpoints:
enabled-by-default: false
web:
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:
info:
enabled: true
@@ -63,21 +67,13 @@ spring:
mcp:
server:
- # Will be enabled once spring-ai-starter-mcp-server is added as dependency
-# name: FHIR MCP Server
-# version: 1.0.0
-# 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
+ name: FHIR MCP Server
+ version: 1.0.0
+ instructions: "This server provides access to a FHIR RESTful API. You can use it to query FHIR resources, perform operations, and retrieve data in a structured format."
enabled: true
+ streamable-http:
+ mcp-endpoint: /mcp/messages
- #endpoint: /mcp
#schema:
# fhir-enabled: true
@@ -93,51 +89,74 @@ spring:
# {{schema}}
#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:
allow-bean-definition-overriding: false
allow-circular-references: true
flyway:
enabled: false
- baselineOnMigrate: true
+ baseline-on-migrate: true
fail-on-missing-locations: false
datasource:
#url: 'jdbc:h2:file:./target/database/h2'
url: jdbc:h2:mem:test_mem
username: sa
password: null
- driverClassName: org.h2.Driver
- max-active: 15
+ driver-class-name: org.h2.Driver
# database connection pool size
hikari:
maximum-pool-size: 10
+ # elasticsearch:
+ # uris: http://localhost:9200
+ # username: elastic
+ # password: changeme
jpa:
properties:
- hibernate.format_sql: false
- hibernate.show_sql: false
+ hibernate:
+ 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.
- #If using H2, then supply the value of ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
- #If using postgres, then supply the value of ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect
- hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
- # hibernate.hbm2ddl.auto: update
- # hibernate.jdbc.batch_size: 20
- # hibernate.cache.use_query_cache: false
- # hibernate.cache.use_second_level_cache: false
- # hibernate.cache.use_structured_entries: false
- # hibernate.cache.use_minimal_puts: false
+ ### elastic parameters ===> see also elasticsearch section below <===
+# backend:
+# type: elasticsearch
+# discovery: true
+# analysis:
+# configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer
+# hosts: localhost:9200
+# protocol: http
+# username: elastic
+# 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:
fhir:
### This flag when enabled to true, will avail evaluate measure operations from CR Module.
@@ -201,42 +220,42 @@ hapi:
fhir_version: R4
### Flag is false by default. This flag enables runtime installation of IG's.
ig_runtime_upload_enabled: false
- ### This flag when enabled to true, will avail evaluate measure operations from CR Module.
+ ### 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
+ ### 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
+ # - example2
# supported_resource_types:
# - Patient
# - Observation
@@ -309,21 +328,21 @@ hapi:
- http://loinc.org/*
- https://loinc.org/*
- ### Uncomment the following section, and any sub-properties you need in order to enable
- ### partitioning support on this server.
- #partitioning:
- # allow_references_across_partitions: false
- # partitioning_include_in_search_hashes: false
- # default_partition_id: 0
+ ### Uncomment the following section, and any sub-properties you need in order to enable
+ ### partitioning support on this server.
+ #partitioning:
+ # allow_references_across_partitions: false
+ # partitioning_include_in_search_hashes: false
+ # default_partition_id: 0
### Enable the following setting to enable Database Partitioning Mode
### See: https://hapifhir.io/hapi-fhir/docs/server_jpa_partitioning/db_partition_mode.html
- # database_partition_mode_enabled: true
+ # database_partition_mode_enabled: true
### Partition Style: Partitioning requires a partition interceptor which helps the server
### select which partition(s) should be accessed for a given request. You can supply your
### own interceptor (see https://hapifhir.io/hapi-fhir/docs/server_jpa_partitioning/partitioning.html#partition-interceptors )
### but the following setting can also be used to use a built-in form.
### Patient ID Partitioning Mode uses the patient/subject ID to determine the partition
- # patient_id_partitioning_mode: true
+ # 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
@@ -432,14 +451,3 @@ hapi:
### 1: NORMALIZED_QUANTITY_STORAGE_SUPPORTED
### 2: NORMALIZED_QUANTITY_SEARCH_SUPPORTED
# normalized_quantity_search_level: 2
-#elasticsearch:
-# debug:
-# pretty_print_json_log: false
-# refresh_after_write: false
-# enabled: false
-# password: SomePassword
-# required_index_status: YELLOW
-# rest_url: 'localhost:9200'
-# protocol: 'http'
-# schema_management_strategy: CREATE
-# username: SomeUsername
diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/CdsHooksServletIT.java b/src/test/java/ca/uhn/fhir/jpa/starter/CdsHooksServletIT.java
index 6a0f8891e53..1e77b3afa2f 100644
--- a/src/test/java/ca/uhn/fhir/jpa/starter/CdsHooksServletIT.java
+++ b/src/test/java/ca/uhn/fhir/jpa/starter/CdsHooksServletIT.java
@@ -46,6 +46,7 @@
}, properties = {
"spring.profiles.include=storageSettingsTest",
"spring.datasource.url=jdbc:h2:mem:dbr4",
+ "spring.jpa.properties.hibernate.search.backend.directory.type=local-heap",
"hapi.fhir.enable_repository_validating_interceptor=true",
"hapi.fhir.fhir_version=r4",
"hapi.fhir.cr.enabled=true",
diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/CustomInterceptorTest.java b/src/test/java/ca/uhn/fhir/jpa/starter/CustomInterceptorTest.java
index daf64384c73..be0415cebb1 100644
--- a/src/test/java/ca/uhn/fhir/jpa/starter/CustomInterceptorTest.java
+++ b/src/test/java/ca/uhn/fhir/jpa/starter/CustomInterceptorTest.java
@@ -14,14 +14,14 @@
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties = {
- "hapi.fhir.custom-bean-packages=some.custom.pkg1",
- "hapi.fhir.custom-interceptor-classes=some.custom.pkg1.CustomInterceptorBean,some.custom.pkg1.CustomInterceptorPojo",
- "spring.datasource.url=jdbc:h2:mem:dbr4",
- "hapi.fhir.cr_enabled=false",
- // "hapi.fhir.enable_repository_validating_interceptor=true",
- "hapi.fhir.fhir_version=r4"
+ "hapi.fhir.custom-bean-packages=some.custom.pkg1",
+ "spring.jpa.properties.hibernate.search.backend.directory.type=local-heap",
+ "hapi.fhir.custom-interceptor-classes=some.custom.pkg1.CustomInterceptorBean,some.custom.pkg1.CustomInterceptorPojo",
+ "spring.datasource.url=jdbc:h2:mem:dbr4",
+ "hapi.fhir.cr_enabled=false",
+ // "hapi.fhir.enable_repository_validating_interceptor=true",
+ "hapi.fhir.fhir_version=r4"
})
-
class CustomInterceptorTest {
@LocalServerPort
diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ElasticsearchLastNR4IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ElasticsearchLastNR4IT.java
index 30b4511a053..c834fc5031c 100644
--- a/src/test/java/ca/uhn/fhir/jpa/starter/ElasticsearchLastNR4IT.java
+++ b/src/test/java/ca/uhn/fhir/jpa/starter/ElasticsearchLastNR4IT.java
@@ -3,8 +3,8 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import ca.uhn.fhir.context.FhirContext;
-import ca.uhn.fhir.jpa.search.lastn.ElasticsearchRestClientFactory;
import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl;
+import ca.uhn.fhir.jpa.starter.elastic.ElasticsearchBootSvcImpl;
import ca.uhn.fhir.jpa.test.config.TestElasticsearchContainerHelper;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
@@ -14,8 +14,6 @@
import java.util.Date;
import java.util.GregorianCalendar;
-import co.elastic.clients.elasticsearch.ElasticsearchClient;
-import co.elastic.clients.elasticsearch.indices.IndexSettings;
import jakarta.annotation.PreDestroy;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
@@ -27,6 +25,8 @@
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@@ -42,6 +42,7 @@
@ExtendWith(SpringExtension.class)
@Testcontainers
+@Disabled
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties =
{
"spring.datasource.url=jdbc:h2:mem:dbr4",
@@ -50,19 +51,19 @@
"hapi.fhir.store_resource_in_lucene_index_enabled=true",
"hapi.fhir.advanced_lucene_indexing=true",
"hapi.fhir.search_index_full_text_enabled=true",
-
- "elasticsearch.enabled=true",
"hapi.fhir.cr_enabled=false",
// Because the port is set randomly, we will set the rest_url using the Initializer.
// "elasticsearch.rest_url='http://localhost:9200'",
- "elasticsearch.username=SomeUsername",
- "elasticsearch.password=SomePassword",
- "elasticsearch.debug.refresh_after_write=true",
- "elasticsearch.protocol=http",
+
+ "spring.elasticsearch.uris=http://localhost:9200",
+ "spring.elasticsearch.username=elastic",
+ "spring.elasticsearch.password=changeme",
"spring.main.allow-bean-definition-overriding=true",
"spring.jpa.properties.hibernate.search.enabled=true",
"spring.jpa.properties.hibernate.search.backend.type=elasticsearch",
- "spring.jpa.properties.hibernate.search.backend.analysis.configurer=ca.uhn.fhir.jpa.search.elastic.HapiElasticsearchAnalysisConfigurer"
+ "spring.jpa.properties.hibernate.search.backend.hosts=localhost:9200",
+ "spring.jpa.properties.hibernate.search.backend.protocol=http",
+ "spring.jpa.properties.hibernate.search.backend.analysis.configurer=ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticsearchAnalysisConfigurer"
})
@ContextConfiguration(initializers = ElasticsearchLastNR4IT.Initializer.class)
class ElasticsearchLastNR4IT {
@@ -73,26 +74,26 @@ class ElasticsearchLastNR4IT {
public static ElasticsearchContainer embeddedElastic = TestElasticsearchContainerHelper.getEmbeddedElasticSearch();
@Autowired
- private ElasticsearchSvcImpl myElasticsearchSvc;
+ private ElasticsearchBootSvcImpl myElasticsearchSvc;
@BeforeAll
public static void beforeClass() throws IOException {
//Given
- ElasticsearchClient elasticsearchHighLevelRestClient = ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient(
- "http", embeddedElastic.getHost() + ":" + embeddedElastic.getMappedPort(9200), "", "");
+ // ElasticsearchClient elasticsearchHighLevelRestClient = ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient(
+// "http", embeddedElastic.getHost() + ":" + embeddedElastic.getMappedPort(9200), "", "");
/* As of 2023-08-10, HAPI FHIR sets SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS to 50000
which is in excess of elastic's default max_result_window. If MAX_SUBSCRIPTION_RESULTS is changed
to a value <= 10000, the following will no longer be necessary. - dotasek
*/
- elasticsearchHighLevelRestClient.indices().putTemplate(t->{
+ /* elasticsearchHighLevelRestClient.indices().putTemplate(t->{
t.name("hapi_fhir_template");
t.indexPatterns("*");
t.settings(new IndexSettings.Builder().maxResultWindow(50000).build());
return t;
});
-
+*/
}
@PreDestroy
@@ -103,7 +104,7 @@ public void stop() {
@LocalServerPort
private int port;
- //@Test
+ @Test
void testLastN() throws IOException, InterruptedException {
Thread.sleep(2000);
@@ -125,6 +126,7 @@ void testLastN() throws IOException, InterruptedException {
IIdType obsId = ourClient.create().resource(obs).execute().getId().toUnqualifiedVersionless();
myElasticsearchSvc.refreshIndex(ElasticsearchSvcImpl.OBSERVATION_INDEX);
+ Thread.sleep(2000);
Parameters output = ourClient.operation().onType(Observation.class).named("lastn")
.withParameter(Parameters.class, "max", new IntegerType(1))
@@ -154,8 +156,10 @@ static class Initializer
public void initialize(
ConfigurableApplicationContext configurableApplicationContext) {
// Since the port is dynamically generated, replace the URL with one that has the correct port
- TestPropertyValues.of("elasticsearch.rest_url=" + embeddedElastic.getHost() +":" + embeddedElastic.getMappedPort(9200))
+ TestPropertyValues.of("spring.elasticsearch.uris=" + embeddedElastic.getHost() +":" + embeddedElastic.getMappedPort(9200))
.applyTo(configurableApplicationContext.getEnvironment());
+ TestPropertyValues.of("spring.jpa.properties.hibernate.search.backend.hosts=" + embeddedElastic.getHost() +":" + embeddedElastic.getMappedPort(9200))
+ .applyTo(configurableApplicationContext.getEnvironment());
}
}
diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDbpmR5IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDbpmR5IT.java
index 0a6a1036c7a..f6ce91162f9 100644
--- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDbpmR5IT.java
+++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDbpmR5IT.java
@@ -29,6 +29,7 @@
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties =
{
"spring.datasource.url=jdbc:h2:mem:dbr5_dbpm",
+ "spring.jpa.properties.hibernate.search.backend.directory.type=local-heap",
"hapi.fhir.fhir_version=r5",
"hapi.fhir.partitioning.database_partition_mode_enabled=true",
"hapi.fhir.partitioning.patient_id_partitioning_mode=true"
diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu2IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu2IT.java
index e8fd9ade47b..97c5922b7c2 100644
--- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu2IT.java
+++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu2IT.java
@@ -21,6 +21,7 @@
"hapi.fhir.fhir_version=dstu2",
"spring.datasource.url=jdbc:h2:mem:dbr2",
"hapi.fhir.cr_enabled=false",
+ "spring.jpa.properties.hibernate.search.backend.directory.type=local-heap"
})
class ExampleServerDstu2IT {
diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu3IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu3IT.java
index f370e8f7389..930001ebfbf 100644
--- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu3IT.java
+++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu3IT.java
@@ -49,7 +49,8 @@
"hapi.fhir.subscription.websocket_enabled=true",
"hapi.fhir.allow_external_references=true",
"hapi.fhir.allow_placeholder_references=true",
- "spring.main.allow-bean-definition-overriding=true"
+ "spring.main.allow-bean-definition-overriding=true",
+ "spring.jpa.properties.hibernate.search.backend.directory.type=local-heap"
})
class ExampleServerDstu3IT implements IServerSupport {
diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4BIT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4BIT.java
index 5a1d2f19cca..e9e90bb40d2 100644
--- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4BIT.java
+++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4BIT.java
@@ -17,16 +17,17 @@
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = {Application.class},
properties = {
- "spring.datasource.url=jdbc:h2:mem:dbr4b",
- "hapi.fhir.enable_repository_validating_interceptor=true",
- "hapi.fhir.fhir_version=r4b",
- "hapi.fhir.subscription.websocket_enabled=false",
- "hapi.fhir.mdm_enabled=false",
- "hapi.fhir.cr_enabled=false",
- // Override is currently required when using MDM as the construction of the MDM
- // beans are ambiguous as they are constructed multiple places. This is evident
- // when running in a spring boot environment
- "spring.main.allow-bean-definition-overriding=true"})
+ "spring.datasource.url=jdbc:h2:mem:dbr4b",
+ "hapi.fhir.enable_repository_validating_interceptor=true",
+ "spring.jpa.properties.hibernate.search.backend.directory.type=local-heap",
+ "hapi.fhir.fhir_version=r4b",
+ "hapi.fhir.subscription.websocket_enabled=false",
+ "hapi.fhir.mdm_enabled=false",
+ "hapi.fhir.cr_enabled=false",
+ // Override is currently required when using MDM as the construction of the MDM
+ // beans are ambiguous as they are constructed multiple places. This is evident
+ // when running in a spring boot environment
+ "spring.main.allow-bean-definition-overriding=true"})
class ExampleServerR4BIT {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExampleServerR4BIT.class);
private IGenericClient ourClient;
@@ -107,7 +108,6 @@ void testBatchPutWithIdenticalTags() {
}
-
@BeforeEach
void beforeEach() {
diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java
index 114f24b38b6..edb0890ffb1 100644
--- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java
+++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java
@@ -54,6 +54,7 @@
RepositoryConfig.class
}, properties = {
"spring.datasource.url=jdbc:h2:mem:dbr4",
+ "spring.ai.mcp.server.enabled=false",
"hapi.fhir.enable_repository_validating_interceptor=true",
"hapi.fhir.fhir_version=r4",
"hapi.fhir.subscription.websocket_enabled=true",
@@ -70,6 +71,9 @@
// beans are ambiguous as they are constructed multiple places. This is evident
// when running in a spring boot environment
"spring.main.allow-bean-definition-overriding=true",
+ "management.health.elasticsearch.enabled=false",
+ "spring.jpa.properties.hibernate.search.backend.directory.type=local-heap",
+ "management.endpoints.web.exposure.include=*",
"hapi.fhir.remote_terminology_service.snomed.system=http://snomed.info/sct",
"hapi.fhir.remote_terminology_service.snomed.url=https://tx.fhir.org/r4"
})
diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR5IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR5IT.java
index f31d003a5f1..2ea57b6ff6e 100644
--- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR5IT.java
+++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR5IT.java
@@ -29,6 +29,7 @@
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties =
{
"spring.datasource.url=jdbc:h2:mem:dbr5",
+ "spring.jpa.properties.hibernate.search.backend.directory.type=local-heap",
"hapi.fhir.fhir_version=r5",
"hapi.fhir.cr_enabled=false",
"hapi.fhir.subscription.websocket_enabled=true",
diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/McpTests.java b/src/test/java/ca/uhn/fhir/jpa/starter/McpTests.java
index 19a8a5f8904..05f990eadad 100644
--- a/src/test/java/ca/uhn/fhir/jpa/starter/McpTests.java
+++ b/src/test/java/ca/uhn/fhir/jpa/starter/McpTests.java
@@ -33,7 +33,7 @@ public void mcpTests() throws JsonProcessingException {
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
.sampling().build()).build();
var initializationResult = client.initialize();
diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/MdmTest.java b/src/test/java/ca/uhn/fhir/jpa/starter/MdmTest.java
index bd1f58db633..4d53428e800 100644
--- a/src/test/java/ca/uhn/fhir/jpa/starter/MdmTest.java
+++ b/src/test/java/ca/uhn/fhir/jpa/starter/MdmTest.java
@@ -8,19 +8,15 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
-import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.nickname.INicknameSvc;
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties = {
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {
"hapi.fhir.fhir_version=r4",
"hapi.fhir.mdm_enabled=true"
})
class MdmTest {
@Autowired
INicknameSvc nicknameService;
-
- @Autowired
- JpaStorageSettings jpaStorageSettings;
@Autowired
SubscriptionSettings subscriptionSettings;
diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/MultitenantServerR4IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/MultitenantServerR4IT.java
index 9233961a08d..3d294655ee9 100644
--- a/src/test/java/ca/uhn/fhir/jpa/starter/MultitenantServerR4IT.java
+++ b/src/test/java/ca/uhn/fhir/jpa/starter/MultitenantServerR4IT.java
@@ -25,6 +25,7 @@
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties =
{
"spring.datasource.url=jdbc:h2:mem:dbr4-mt",
+ "spring.jpa.properties.hibernate.search.backend.directory.type=local-heap",
"hapi.fhir.fhir_version=r4",
"hapi.fhir.subscription.websocket_enabled=true",
"hapi.fhir.cr_enabled=false",
diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ParallelUpdatesVersionConflictTest.java b/src/test/java/ca/uhn/fhir/jpa/starter/ParallelUpdatesVersionConflictTest.java
index 49305fa7bb1..d0f2334a2f6 100644
--- a/src/test/java/ca/uhn/fhir/jpa/starter/ParallelUpdatesVersionConflictTest.java
+++ b/src/test/java/ca/uhn/fhir/jpa/starter/ParallelUpdatesVersionConflictTest.java
@@ -30,7 +30,8 @@
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties = {
"spring.datasource.url=jdbc:h2:mem:dbr4",
"hapi.fhir.fhir_version=r4",
- "hapi.fhir.userRequestRetryVersionConflictsInterceptorEnabled=true"
+ "hapi.fhir.userRequestRetryVersionConflictsInterceptorEnabled=true",
+ "spring.jpa.properties.hibernate.search.backend.directory.type=local-heap"
})
/**
diff --git a/src/test/resources/application.yaml b/src/test/resources/application-test.yaml
similarity index 71%
rename from src/test/resources/application.yaml
rename to src/test/resources/application-test.yaml
index 85e78863261..1e08a224386 100644
--- a/src/test/resources/application.yaml
+++ b/src/test/resources/application-test.yaml
@@ -1,83 +1,4 @@
-management:
- #The following configuration will enable the actuator endpoints at /actuator/health, /actuator/info, /actuator/prometheus
- endpoints:
- enabled-by-default: false
- web:
- exposure:
- include: 'info,health,prometheus,metrics' # or '*' for all
- endpoint:
- info:
- enabled: true
- metrics:
- enabled: true
- health:
- enabled: true
- probes:
- enabled: true
- group:
- liveness:
- include:
- - livenessState
- - readinessState
- prometheus:
- enabled: true
- prometheus:
- metrics:
- export:
- enabled: true
-spring:
- main:
- allow-circular-references: true
- allow-bean-definition-overriding: true
- flyway:
- enabled: false
- fail-on-missing-locations: false
- baselineOnMigrate: true
- datasource:
- url: jdbc:h2:mem:test_mem
- username: sa
- password: null
- driverClassName: org.h2.Driver
- max-active: 15
- # database connection pool size
- hikari:
- maximum-pool-size: 10
- jpa:
- properties:
- hibernate.format_sql: false
- hibernate.show_sql: false
-
- #########################################
- # Hibernate Dialect Setting
- #########################################
- # Use one of the following values:
- # ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
- # ca.uhn.fhir.jpa.model.dialect.HapiFhirDerbyDialect
- # ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect
- # ca.uhn.fhir.jpa.model.dialect.HapiFhirOracleDialect
- # ca.uhn.fhir.jpa.model.dialect.HapiFhirSQLServerDialect
- # ca.uhn.fhir.jpa.model.dialect.HapiFhirMySQLDialect (Deprecated!)
- #########################################
- hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
- #########################################
- # hibernate.hbm2ddl.auto: update
- # hibernate.jdbc.batch_size: 20
- # hibernate.cache.use_query_cache: false
- # hibernate.cache.use_second_level_cache: false
- # hibernate.cache.use_structured_entries: false
- # hibernate.cache.use_minimal_puts: false
- ### These settings will enable fulltext search with lucene or elastic
- hibernate.search.enabled: false
- ### lucene parameters
- # hibernate.search.backend.type: lucene
- # hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiLuceneAnalysisConfigurer
- # hibernate.search.backend.directory.type: local-filesystem
- # hibernate.search.backend.directory.root: target/lucenefiles
- # hibernate.search.backend.lucene_version: lucene_current
- ### elastic parameters ===> see also elasticsearch section below <===
-# hibernate.search.backend.type: elasticsearch
-# hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer
hapi:
fhir: