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 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 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 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: