Feature/elastic on boot (#856)

* Fixing up elastic for Spring Boot

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

* Formatting

* corrected condition

* fix import

* fix2

* crappy fix3

* fix actuator endpoint

* Simplified expression

* Ironed out a few legacy issues

* more rework

* major overhaul

* Disabling invalid test

* Reverting back to defaults for text searches

* Added default hibernate settings from the EnvironmentHelper

* Formatting

* Added comment on class shadow

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

View File

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

View File

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

View File

@@ -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();

View File

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

View File

@@ -9,6 +9,7 @@ import ca.uhn.fhir.jpa.model.config.PartitionSettings.CrossPartitionReferenceMod
import ca.uhn.fhir.jpa.model.config.SubscriptionSettings;
import ca.uhn.fhir.jpa.model.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.hl7.fhir.r4.model.Bundle.BundleType;
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 @@ import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
*/
@Configuration
@EnableTransactionManagement
@Import(ElasticsearchBootSvcImpl.class)
public class FhirServerConfigCommon {
private static final Logger ourLog = LoggerFactory.getLogger(FhirServerConfigCommon.class);
@@ -274,7 +273,15 @@ public class FhirServerConfigCommon {
ourLog.debug("Server configured to Store Meta Source: {}", appProperties.getStore_meta_source_information());
jpaStorageSettings.setStoreMetaSourceInformation(appProperties.getStore_meta_source_information());
storageSettings(appProperties, jpaStorageSettings);
jpaStorageSettings.setAllowContainsSearches(appProperties.getAllow_contains_searches());
jpaStorageSettings.setAllowExternalReferences(appProperties.getAllow_external_references());
jpaStorageSettings.setDefaultSearchParamsCanBeOverridden(
appProperties.getAllow_override_default_search_params());
jpaStorageSettings.setNormalizedQuantitySearchLevel(appProperties.getNormalized_quantity_search_level());
jpaStorageSettings.setIndexOnContainedResources(appProperties.getEnable_index_contained_resource());
jpaStorageSettings.setIndexIdentifierOfType(appProperties.getEnable_index_of_type());
return jpaStorageSettings;
}
@@ -332,19 +339,6 @@ public class FhirServerConfigCommon {
return new JpaHibernatePropertiesProvider(myEntityManagerFactory);
}
protected StorageSettings storageSettings(AppProperties appProperties, JpaStorageSettings jpaStorageSettings) {
jpaStorageSettings.setAllowContainsSearches(appProperties.getAllow_contains_searches());
jpaStorageSettings.setAllowExternalReferences(appProperties.getAllow_external_references());
jpaStorageSettings.setDefaultSearchParamsCanBeOverridden(
appProperties.getAllow_override_default_search_params());
jpaStorageSettings.setNormalizedQuantitySearchLevel(appProperties.getNormalized_quantity_search_level());
jpaStorageSettings.setIndexOnContainedResources(appProperties.getEnable_index_contained_resource());
jpaStorageSettings.setIndexIdentifierOfType(appProperties.getEnable_index_of_type());
return jpaStorageSettings;
}
@Lazy
@Bean
public IBinaryStorageSvc binaryStorageSvc(AppProperties appProperties) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 @@ import org.springframework.core.type.AnnotatedTypeMetadata;
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;
}

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

@@ -7,16 +7,18 @@ import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import 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;

View File

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

View File

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

View File

@@ -6,10 +6,12 @@ import ca.uhn.fhir.jpa.ips.generator.IIpsGeneratorSvc;
import ca.uhn.fhir.jpa.ips.generator.IpsGeneratorSvcImpl;
import ca.uhn.fhir.jpa.ips.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() {

View File

@@ -8,14 +8,14 @@ import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceRegistry;
import ca.uhn.hapi.fhir.cdshooks.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 @@ import java.util.List;
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<McpBridge> mcpBridges, McpStreamableServerTransportProvider transportProvider) {
return McpServer.sync(transportProvider)
.tools(mcpBridges.stream()
.flatMap(bridge -> bridge.generateTools().stream())
.toList())
.build();
public List<McpServerFeatures.SyncToolSpecification> syncServer(List<McpBridge> mcpBridges) {
return mcpBridges.stream()
.flatMap(bridge -> bridge.generateTools().stream())
.toList();
}
@Bean
@@ -63,11 +61,11 @@ public class McpServerConfig {
@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 class McpServerConfig {
@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);
}
}

View File

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

View File

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

View File

@@ -1,22 +1,7 @@
package ca.uhn.fhir.jpa.starter.util;
import ca.uhn.fhir.jpa.config.HapiFhirLocalContainerEntityManagerFactoryBean;
import ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers;
import ca.uhn.fhir.jpa.search.elastic.ElasticsearchHibernatePropertiesBuilder;
import org.apache.lucene.util.Version;
import org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings;
import org.hibernate.search.backend.elasticsearch.index.IndexStatus;
import org.hibernate.search.backend.lucene.cfg.LuceneBackendSettings;
import org.hibernate.search.backend.lucene.cfg.LuceneIndexSettings;
import org.hibernate.search.backend.lucene.lowlevel.directory.impl.LocalFileSystemDirectoryProvider;
import org.hibernate.search.engine.cfg.BackendSettings;
import org.hibernate.search.mapper.orm.automaticindexing.session.AutomaticIndexingSynchronizationStrategyNames;
import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
import org.hibernate.search.mapper.orm.schema.management.SchemaManagementStrategyName;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
@@ -25,142 +10,11 @@ import org.springframework.core.env.PropertySource;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import static java.util.Objects.requireNonNullElse;
public class EnvironmentHelper {
public static Properties getHibernateProperties(
ConfigurableEnvironment environment, ConfigurableListableBeanFactory myConfigurableListableBeanFactory) {
Properties properties = new Properties();
Map<String, Object> jpaProps = getPropertiesStartingWith(environment, "spring.jpa.properties");
for (Map.Entry<String, Object> entry : jpaProps.entrySet()) {
String strippedKey = entry.getKey().replace("spring.jpa.properties.", "");
properties.put(strippedKey, entry.getValue().toString());
}
// also check for JPA properties set as environment variables, this is slightly hacky and doesn't cover all
// the naming conventions Springboot allows
// but there doesn't seem to be a better/deterministic way to get these properties when they are set as ENV
// variables and this at least provides
// a way to set them (in a docker container, for instance)
Map<String, Object> jpaPropsEnv = getPropertiesStartingWith(environment, "SPRING_JPA_PROPERTIES");
for (Map.Entry<String, Object> entry : jpaPropsEnv.entrySet()) {
String strippedKey = entry.getKey().replace("SPRING_JPA_PROPERTIES_", "");
strippedKey = strippedKey.replaceAll("_", ".");
strippedKey = strippedKey.toLowerCase();
properties.put(strippedKey, entry.getValue().toString());
}
// Spring Boot Autoconfiguration defaults
properties.putIfAbsent(AvailableSettings.SCANNER, "org.hibernate.boot.archive.scan.internal.DisabledScanner");
properties.putIfAbsent(
AvailableSettings.IMPLICIT_NAMING_STRATEGY, SpringImplicitNamingStrategy.class.getName());
properties.putIfAbsent(
AvailableSettings.PHYSICAL_NAMING_STRATEGY, CamelCaseToUnderscoresNamingStrategy.class.getName());
// TODO The bean factory should be added as parameter but that requires that it can be injected from the
// entityManagerFactory bean from xBaseConfig
// properties.putIfAbsent(AvailableSettings.BEAN_CONTAINER, new SpringBeanContainer(beanFactory));
// hapi-fhir-jpaserver-base "sensible defaults"
Map<String, Object> hapiJpaPropertyMap = new HapiFhirLocalContainerEntityManagerFactoryBean(
myConfigurableListableBeanFactory)
.getJpaPropertyMap();
hapiJpaPropertyMap.forEach(properties::putIfAbsent);
// hapi-fhir-jpaserver-starter defaults
properties.putIfAbsent(AvailableSettings.FORMAT_SQL, false);
properties.putIfAbsent(AvailableSettings.SHOW_SQL, false);
properties.putIfAbsent(AvailableSettings.HBM2DDL_AUTO, "update");
properties.putIfAbsent(AvailableSettings.STATEMENT_BATCH_SIZE, 20);
properties.putIfAbsent(AvailableSettings.USE_QUERY_CACHE, false);
properties.putIfAbsent(AvailableSettings.USE_SECOND_LEVEL_CACHE, false);
properties.putIfAbsent(AvailableSettings.USE_STRUCTURED_CACHE, false);
properties.putIfAbsent(AvailableSettings.USE_MINIMAL_PUTS, false);
// Hibernate Search defaults
properties.putIfAbsent(HibernateOrmMapperSettings.ENABLED, false);
if (Boolean.parseBoolean(String.valueOf(properties.get(HibernateOrmMapperSettings.ENABLED)))) {
if (isElasticsearchEnabled(environment)) {
properties.putIfAbsent(
BackendSettings.backendKey(BackendSettings.TYPE), ElasticsearchBackendSettings.TYPE_NAME);
} else {
properties.putIfAbsent(
BackendSettings.backendKey(BackendSettings.TYPE), LuceneBackendSettings.TYPE_NAME);
}
if (properties
.get(BackendSettings.backendKey(BackendSettings.TYPE))
.equals(LuceneBackendSettings.TYPE_NAME)) {
properties.putIfAbsent(
BackendSettings.backendKey(LuceneIndexSettings.DIRECTORY_TYPE),
LocalFileSystemDirectoryProvider.NAME);
properties.putIfAbsent(
BackendSettings.backendKey(LuceneIndexSettings.DIRECTORY_ROOT), "target/lucenefiles");
properties.putIfAbsent(
BackendSettings.backendKey(LuceneBackendSettings.ANALYSIS_CONFIGURER),
HapiHSearchAnalysisConfigurers.HapiLuceneAnalysisConfigurer.class.getName());
properties.putIfAbsent(
BackendSettings.backendKey(LuceneBackendSettings.LUCENE_VERSION), Version.LATEST);
} else if (properties
.get(BackendSettings.backendKey(BackendSettings.TYPE))
.equals(ElasticsearchBackendSettings.TYPE_NAME)) {
ElasticsearchHibernatePropertiesBuilder builder = new ElasticsearchHibernatePropertiesBuilder();
IndexStatus requiredIndexStatus =
environment.getProperty("elasticsearch.required_index_status", IndexStatus.class);
builder.setRequiredIndexStatus(requireNonNullElse(requiredIndexStatus, IndexStatus.YELLOW));
builder.setHosts(getElasticsearchServerUrl(environment));
builder.setUsername(getElasticsearchServerUsername(environment));
builder.setPassword(getElasticsearchServerPassword(environment));
builder.setProtocol(getElasticsearchServerProtocol(environment));
SchemaManagementStrategyName indexSchemaManagementStrategy = environment.getProperty(
"elasticsearch.schema_management_strategy", SchemaManagementStrategyName.class);
builder.setIndexSchemaManagementStrategy(
requireNonNullElse(indexSchemaManagementStrategy, SchemaManagementStrategyName.CREATE));
Boolean refreshAfterWrite =
environment.getProperty("elasticsearch.debug.refresh_after_write", Boolean.class);
if (refreshAfterWrite == null || !refreshAfterWrite) {
builder.setDebugIndexSyncStrategy(AutomaticIndexingSynchronizationStrategyNames.ASYNC);
} else {
builder.setDebugIndexSyncStrategy(AutomaticIndexingSynchronizationStrategyNames.READ_SYNC);
}
builder.setDebugPrettyPrintJsonLog(requireNonNullElse(
environment.getProperty("elasticsearch.debug.pretty_print_json_log", Boolean.class), false));
builder.apply(properties);
} else {
throw new UnsupportedOperationException("Unsupported Hibernate Search backend: "
+ properties.get(BackendSettings.backendKey(BackendSettings.TYPE)));
}
}
return properties;
}
public static String getElasticsearchServerUrl(ConfigurableEnvironment environment) {
return environment.getProperty("elasticsearch.rest_url", String.class);
}
public static String getElasticsearchServerProtocol(ConfigurableEnvironment environment) {
return environment.getProperty("elasticsearch.protocol", String.class, "http");
}
public static String getElasticsearchServerUsername(ConfigurableEnvironment environment) {
return environment.getProperty("elasticsearch.username");
}
public static String getElasticsearchServerPassword(ConfigurableEnvironment environment) {
return environment.getProperty("elasticsearch.password");
}
public static Boolean isElasticsearchEnabled(ConfigurableEnvironment environment) {
if (environment.getProperty("elasticsearch.enabled", Boolean.class) != null) {
return environment.getProperty("elasticsearch.enabled", Boolean.class);
} else {
return false;
}
public static <T> T getConfiguration(ConditionContext context, String path, Class<T> clazz) {
return Binder.get(context.getEnvironment()).bind(path, clazz).orElse(null);
}
public static Map<String, Object> getPropertiesStartingWith(ConfigurableEnvironment aEnv, String aKeyPrefix) {

View File

@@ -9,12 +9,16 @@ server:
#Adds the option to go to e.g. http://localhost:8080/actuator/health for seeing the running configuration
#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