Merged the rel_5_3_0 branch into this Branch to pick up the MDM changes.

This commit is contained in:
Kevin Dougan
2021-01-07 09:12:52 -05:00
parent a83927aa0c
commit 3954f6a3f4
18 changed files with 338 additions and 262 deletions

View File

@@ -1,5 +1,5 @@
[*.java] [*.java]
charset = utf-8 charset = utf-8
indent_style = space indent_style = tab
indent_size = 2 indent_size = 3

View File

@@ -320,6 +320,10 @@ The server may be configured with subscription support by enabling properties in
Set `hapi.fhir.cql_enabled=true` in the [application.yaml](https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/application.yaml) file to enable [Clinical Quality Language](https://cql.hl7.org/) on this server. Set `hapi.fhir.cql_enabled=true` in the [application.yaml](https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/application.yaml) file to enable [Clinical Quality Language](https://cql.hl7.org/) on this server.
## Enabling MDM (EMPI)
Set `hapi.fhir.mdm_enabled=true` in the [application.yaml](https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/application.yaml) file to enable MDM on this server. The MDM matching rules are configured in [mdm-rules.json](https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/mdm-rules.json). The rules in this example file should be replaced with actual matching rules appropriate to your data. Note that MDM relies on subscriptions, so for MDM to work, subscriptions must be enabled.
## Using Elasticsearch ## Using Elasticsearch
By default, the server will use embedded lucene indexes for terminology and fulltext indexing purposes. You can switch this to using lucene by editing the properties in [application.yaml](https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/application.yaml) By default, the server will use embedded lucene indexes for terminology and fulltext indexing purposes. You can switch this to using lucene by editing the properties in [application.yaml](https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/application.yaml)
@@ -328,9 +332,10 @@ For example:
```properties ```properties
elasticsearch.enabled=true elasticsearch.enabled=true
elasticsearch.rest_url=http://localhost:9200 elasticsearch.rest_url=localhost:9200
elasticsearch.username=SomeUsername elasticsearch.username=SomeUsername
elasticsearch.password=SomePassword elasticsearch.password=SomePassword
elasticsearch.protocol=http
elasticsearch.required_index_status=YELLOW elasticsearch.required_index_status=YELLOW
elasticsearch.schema_management_strategy=CREATE elasticsearch.schema_management_strategy=CREATE
``` ```

23
pom.xml
View File

@@ -22,7 +22,6 @@
<properties> <properties>
<java.version>8</java.version> <java.version>8</java.version>
<spring_boot_version>2.3.4.RELEASE</spring_boot_version>
</properties> </properties>
<prerequisites> <prerequisites>
@@ -107,6 +106,12 @@
<artifactId>hapi-fhir-jpaserver-cql</artifactId> <artifactId>hapi-fhir-jpaserver-cql</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<!-- This dependency includes the JPA MDM Server -->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-mdm</artifactId>
<version>${project.version}</version>
</dependency>
<!-- This dependency is used for the "FHIR Tester" web app overlay --> <!-- This dependency is used for the "FHIR Tester" web app overlay -->
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
@@ -235,6 +240,21 @@
<artifactId>jetty-webapp</artifactId> <artifactId>jetty-webapp</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>elasticsearch</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!-- <!--
@@ -358,7 +378,6 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId> <artifactId>maven-failsafe-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration> <configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile> <redirectTestOutputToFile>true</redirectTestOutputToFile>
</configuration> </configuration>

View File

@@ -0,0 +1,45 @@
package ca.uhn.fhir.jpa.mdm;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.mdm.config.MdmConsumerConfig;
import ca.uhn.fhir.jpa.mdm.config.MdmSubmitterConfig;
import ca.uhn.fhir.jpa.starter.AppProperties;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.rules.config.MdmRuleValidator;
import ca.uhn.fhir.mdm.rules.config.MdmSettings;
import ca.uhn.fhir.rest.server.util.ISearchParamRetriever;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
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;
import org.springframework.core.io.Resource;
import java.io.IOException;
/**
* TODO: Move this to package "ca.uhn.fhir.jpa.starter" in HAPI FHIR 5.2.0+. The lousy component scan
* in 5.1.0 picks this up even if MDM is disabled currently.
*/
@Configuration
@Conditional(MdmConfigCondition.class)
@Import({MdmConsumerConfig.class, MdmSubmitterConfig.class})
public class MdmConfig {
@Bean
MdmRuleValidator mdmRuleValidator(FhirContext theFhirContext, ISearchParamRetriever theSearchParamRetriever) {
return new MdmRuleValidator(theFhirContext, theSearchParamRetriever);
}
@Bean
IMdmSettings mdmSettings(@Autowired MdmRuleValidator theMdmRuleValidator, AppProperties appProperties) throws IOException {
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
Resource resource = resourceLoader.getResource("mdm-rules.json");
String json = IOUtils.toString(resource.getInputStream(), Charsets.UTF_8);
return new MdmSettings(theMdmRuleValidator).setEnabled(appProperties.getMdm_enabled()).setScriptText(json);
}
}

View File

@@ -0,0 +1,13 @@
package ca.uhn.fhir.jpa.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

@@ -23,6 +23,7 @@ import org.springframework.context.annotation.Configuration;
public class AppProperties { public class AppProperties {
private Boolean cql_enabled = false; private Boolean cql_enabled = false;
private Boolean mdm_enabled = false;
private Boolean allow_cascading_deletes = false; private Boolean allow_cascading_deletes = false;
private Boolean allow_contains_searches = true; private Boolean allow_contains_searches = true;
private Boolean allow_external_references = false; private Boolean allow_external_references = false;
@@ -31,6 +32,7 @@ public class AppProperties {
private Boolean allow_placeholder_references = true; private Boolean allow_placeholder_references = true;
private Boolean auto_create_placeholder_reference_targets = true; private Boolean auto_create_placeholder_reference_targets = true;
private Boolean enable_index_missing_fields = false; private Boolean enable_index_missing_fields = false;
private Boolean enable_repository_validating_interceptor = false;
private Boolean enforce_referential_integrity_on_delete = true; private Boolean enforce_referential_integrity_on_delete = true;
private Boolean enforce_referential_integrity_on_write = true; private Boolean enforce_referential_integrity_on_write = true;
private Boolean etag_support_enabled = true; private Boolean etag_support_enabled = true;
@@ -97,6 +99,14 @@ public class AppProperties {
this.cql_enabled = cql_enabled; this.cql_enabled = cql_enabled;
} }
public Boolean getMdm_enabled() {
return mdm_enabled;
}
public void setMdm_enabled(Boolean mdm_enabled) {
this.mdm_enabled = mdm_enabled;
}
public Cors getCors() { public Cors getCors() {
return cors; return cors;
} }
@@ -244,6 +254,14 @@ public class AppProperties {
this.enable_index_missing_fields = enable_index_missing_fields; this.enable_index_missing_fields = enable_index_missing_fields;
} }
public Boolean getEnable_repository_validating_interceptor() {
return enable_repository_validating_interceptor;
}
public void setEnable_repository_validating_interceptor(Boolean theEnable_repository_validating_interceptor) {
enable_repository_validating_interceptor = theEnable_repository_validating_interceptor;
}
public Boolean getEnforce_referential_integrity_on_delete() { public Boolean getEnforce_referential_integrity_on_delete() {
return enforce_referential_integrity_on_delete; return enforce_referential_integrity_on_delete;
} }

View File

@@ -1,5 +1,6 @@
package ca.uhn.fhir.jpa.starter; package ca.uhn.fhir.jpa.starter;
import ca.uhn.fhir.jpa.mdm.MdmConfig;
import ca.uhn.fhir.jpa.starter.annotations.OnEitherVersion; import ca.uhn.fhir.jpa.starter.annotations.OnEitherVersion;
import ca.uhn.fhir.jpa.subscription.channel.config.SubscriptionChannelConfig; import ca.uhn.fhir.jpa.subscription.channel.config.SubscriptionChannelConfig;
import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig; import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig;
@@ -23,7 +24,7 @@ import org.springframework.web.servlet.DispatcherServlet;
@ServletComponentScan(basePackageClasses = { @ServletComponentScan(basePackageClasses = {
JpaRestfulServer.class}) JpaRestfulServer.class})
@SpringBootApplication(exclude = {ElasticsearchRestClientAutoConfiguration.class}) @SpringBootApplication(exclude = {ElasticsearchRestClientAutoConfiguration.class})
@Import({SubscriptionSubmitterConfig.class, SubscriptionProcessorConfig.class, SubscriptionChannelConfig.class, WebsocketDispatcherConfig.class}) @Import({SubscriptionSubmitterConfig.class, SubscriptionProcessorConfig.class, SubscriptionChannelConfig.class, WebsocketDispatcherConfig.class, MdmConfig.class})
public class Application extends SpringBootServletInitializer { public class Application extends SpringBootServletInitializer {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -11,6 +11,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor; import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor;
import ca.uhn.fhir.jpa.bulk.provider.BulkDataExportProvider; import ca.uhn.fhir.jpa.bulk.provider.BulkDataExportProvider;
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor; import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingInterceptor;
import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc; import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc;
import ca.uhn.fhir.jpa.packages.PackageInstallationSpec; import ca.uhn.fhir.jpa.packages.PackageInstallationSpec;
import ca.uhn.fhir.jpa.partition.PartitionManagementProvider; import ca.uhn.fhir.jpa.partition.PartitionManagementProvider;
@@ -365,6 +366,12 @@ public class BaseJpaRestfulServer extends RestfulServer {
daoConfig.setLastNEnabled(true); daoConfig.setLastNEnabled(true);
} }
// Repository Validating Interceptor
if (Boolean.TRUE.equals(appProperties.getEnable_repository_validating_interceptor())) {
RepositoryValidationInterceptorFactory repositoryValidationInterceptorFactory = myApplicationContext.getBean(RepositoryValidationInterceptorFactory.class);
RepositoryValidatingInterceptor interceptor = repositoryValidationInterceptorFactory.build();
interceptorService.registerInterceptor(interceptor);
}
} }

View File

@@ -1,8 +1,16 @@
package ca.uhn.fhir.jpa.starter; package ca.uhn.fhir.jpa.starter;
import ca.uhn.fhir.jpa.search.HapiLuceneAnalysisConfigurer;
import ca.uhn.fhir.jpa.search.elastic.ElasticsearchHibernatePropertiesBuilder; import ca.uhn.fhir.jpa.search.elastic.ElasticsearchHibernatePropertiesBuilder;
import org.hibernate.search.elasticsearch.cfg.ElasticsearchIndexStatus; import org.apache.commons.lang3.StringUtils;
import org.hibernate.search.elasticsearch.cfg.IndexSchemaManagementStrategy; import org.elasticsearch.action.admin.indices.stats.IndexStats;
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.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.core.env.CompositePropertySource; import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.EnumerablePropertySource;
@@ -19,7 +27,6 @@ public class EnvironmentHelper {
Properties properties = new Properties(); Properties properties = new Properties();
if (environment.getProperty("spring.jpa.properties", String.class) == null) { if (environment.getProperty("spring.jpa.properties", String.class) == null) {
properties.put("hibernate.search.model_mapping", "ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory");
properties.put("hibernate.format_sql", "false"); properties.put("hibernate.format_sql", "false");
properties.put("hibernate.show_sql", "false"); properties.put("hibernate.show_sql", "false");
properties.put("hibernate.hbm2ddl.auto", "update"); properties.put("hibernate.hbm2ddl.auto", "update");
@@ -28,11 +35,18 @@ public class EnvironmentHelper {
properties.put("hibernate.cache.use_second_level_cache", "false"); properties.put("hibernate.cache.use_second_level_cache", "false");
properties.put("hibernate.cache.use_structured_entries", "false"); properties.put("hibernate.cache.use_structured_entries", "false");
properties.put("hibernate.cache.use_minimal_puts", "false"); properties.put("hibernate.cache.use_minimal_puts", "false");
properties.put("hibernate.search.default.directory_provider", "filesystem");
properties.put("hibernate.search.default.indexBase", "target/lucenefiles"); properties.put(BackendSettings.backendKey(LuceneIndexSettings.DIRECTORY_TYPE), "local-filesystem");
properties.put("hibernate.search.lucene_version", "LUCENE_CURRENT"); properties.put(BackendSettings.backendKey(LuceneIndexSettings.DIRECTORY_ROOT), "target/lucenefiles");
properties.put(BackendSettings.backendKey(BackendSettings.TYPE), "lucene");
properties.put(BackendSettings.backendKey(LuceneBackendSettings.ANALYSIS_CONFIGURER), HapiLuceneAnalysisConfigurer.class.getName());
properties.put(BackendSettings.backendKey(LuceneBackendSettings.LUCENE_VERSION), "LUCENE_CURRENT");
//Set this value to true in the properties to enable lucene.
properties.put(HibernateOrmMapperSettings.ENABLED, environment.getProperty("spring.jpa.properties.hibernate.search.enabled", "false"));
} else { } else {
Arrays.asList(environment.getProperty("spring.jpa.properties", String.class).split(" ")).stream().forEach(s -> Arrays.asList(environment.getProperty("spring.jpa.properties", String.class).split(" ")).stream().filter(s -> !StringUtils.isEmpty(s)).forEach(s ->
{ {
String[] values = s.split("="); String[] values = s.split("=");
properties.put(values[0], values[1]); properties.put(values[0], values[1]);
@@ -42,9 +56,9 @@ public class EnvironmentHelper {
if (environment.getProperty("elasticsearch.enabled", Boolean.class) != null if (environment.getProperty("elasticsearch.enabled", Boolean.class) != null
&& environment.getProperty("elasticsearch.enabled", Boolean.class) == true ){ && environment.getProperty("elasticsearch.enabled", Boolean.class) == true ){
ElasticsearchHibernatePropertiesBuilder builder = new ElasticsearchHibernatePropertiesBuilder(); ElasticsearchHibernatePropertiesBuilder builder = new ElasticsearchHibernatePropertiesBuilder();
ElasticsearchIndexStatus requiredIndexStatus = environment.getProperty("elasticsearch.required_index_status", ElasticsearchIndexStatus.class); IndexStatus requiredIndexStatus = environment.getProperty("elasticsearch.required_index_status", IndexStatus.class);
if (requiredIndexStatus == null) { if (requiredIndexStatus == null) {
builder.setRequiredIndexStatus(ElasticsearchIndexStatus.YELLOW); builder.setRequiredIndexStatus(IndexStatus.YELLOW);
} else { } else {
builder.setRequiredIndexStatus(requiredIndexStatus); builder.setRequiredIndexStatus(requiredIndexStatus);
} }
@@ -52,18 +66,19 @@ public class EnvironmentHelper {
builder.setRestUrl(getElasticsearchServerUrl(environment)); builder.setRestUrl(getElasticsearchServerUrl(environment));
builder.setUsername(getElasticsearchServerUsername(environment)); builder.setUsername(getElasticsearchServerUsername(environment));
builder.setPassword(getElasticsearchServerPassword(environment)); builder.setPassword(getElasticsearchServerPassword(environment));
IndexSchemaManagementStrategy indexSchemaManagementStrategy = environment.getProperty("elasticsearch.schema_management_strategy", IndexSchemaManagementStrategy.class); builder.setProtocol(getElasticsearchServerProtocol(environment));
SchemaManagementStrategyName indexSchemaManagementStrategy = environment.getProperty("elasticsearch.schema_management_strategy", SchemaManagementStrategyName.class);
if (indexSchemaManagementStrategy == null) { if (indexSchemaManagementStrategy == null) {
builder.setIndexSchemaManagementStrategy(IndexSchemaManagementStrategy.CREATE); builder.setIndexSchemaManagementStrategy(SchemaManagementStrategyName.CREATE);
} else { } else {
builder.setIndexSchemaManagementStrategy(indexSchemaManagementStrategy); builder.setIndexSchemaManagementStrategy(indexSchemaManagementStrategy);
} }
// pretty_print_json_log: false // pretty_print_json_log: false
Boolean refreshAfterWrite = environment.getProperty("elasticsearch.debug.refresh_after_write", Boolean.class); Boolean refreshAfterWrite = environment.getProperty("elasticsearch.debug.refresh_after_write", Boolean.class);
if (refreshAfterWrite == null) { if (refreshAfterWrite == null || refreshAfterWrite == false) {
builder.setDebugRefreshAfterWrite(false); builder.setDebugIndexSyncStrategy(AutomaticIndexingSynchronizationStrategyNames.ASYNC);
} else { } else {
builder.setDebugRefreshAfterWrite(refreshAfterWrite); builder.setDebugIndexSyncStrategy(AutomaticIndexingSynchronizationStrategyNames.READ_SYNC);
} }
// pretty_print_json_log: false // pretty_print_json_log: false
Boolean prettyPrintJsonLog = environment.getProperty("elasticsearch.debug.pretty_print_json_log", Boolean.class); Boolean prettyPrintJsonLog = environment.getProperty("elasticsearch.debug.pretty_print_json_log", Boolean.class);
@@ -74,7 +89,6 @@ public class EnvironmentHelper {
} }
builder.apply(properties); builder.apply(properties);
} }
return properties; return properties;
} }
@@ -82,6 +96,10 @@ public class EnvironmentHelper {
return environment.getProperty("elasticsearch.rest_url", String.class); 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) { public static String getElasticsearchServerUsername(ConfigurableEnvironment environment) {
return environment.getProperty("elasticsearch.username"); return environment.getProperty("elasticsearch.username");
} }

View File

@@ -3,7 +3,7 @@ package ca.uhn.fhir.jpa.starter;
import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.binstore.DatabaseBlobBinaryStorageSvcImpl; import ca.uhn.fhir.jpa.binstore.DatabaseBlobBinaryStorageSvcImpl;
import ca.uhn.fhir.jpa.binstore.IBinaryStorageSvc; import ca.uhn.fhir.jpa.binstore.IBinaryStorageSvc;
import ca.uhn.fhir.jpa.config.HibernateDialectProvider; import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider;
import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionDeliveryHandlerFactory; import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionDeliveryHandlerFactory;
@@ -132,10 +132,17 @@ public class FhirServerConfigCommon {
} }
@Bean
@Lazy
public RepositoryValidationInterceptorFactory repositoryValidationInterceptorFactory() {
return new RepositoryValidationInterceptorFactory();
}
@Primary @Primary
@Bean @Bean
public HibernateDialectProvider jpaStarterDialectProvider(LocalContainerEntityManagerFactoryBean myEntityManagerFactory) { public HibernatePropertiesProvider jpaStarterDialectProvider(LocalContainerEntityManagerFactoryBean myEntityManagerFactory) {
return new JpaHibernateDialectProvider(myEntityManagerFactory); return new JpaHibernatePropertiesProvider(myEntityManagerFactory);
} }
@Bean @Bean

View File

@@ -70,7 +70,13 @@ public class FhirServerConfigR4 extends BaseJavaConfigR4 {
public ElasticsearchSvcImpl elasticsearchSvc() { public ElasticsearchSvcImpl elasticsearchSvc() {
if (EnvironmentHelper.isElasticsearchEnabled(configurableEnvironment)) { if (EnvironmentHelper.isElasticsearchEnabled(configurableEnvironment)) {
String elasticsearchUrl = EnvironmentHelper.getElasticsearchServerUrl(configurableEnvironment); String elasticsearchUrl = EnvironmentHelper.getElasticsearchServerUrl(configurableEnvironment);
String elasticsearchHost = elasticsearchUrl.substring(elasticsearchUrl.indexOf("://")+3, elasticsearchUrl.lastIndexOf(":")); String elasticsearchHost;
if (elasticsearchUrl.startsWith("http")) {
elasticsearchHost = elasticsearchUrl.substring(elasticsearchUrl.indexOf("://") + 3, elasticsearchUrl.lastIndexOf(":"));
} else {
elasticsearchHost = elasticsearchUrl.substring(0, elasticsearchUrl.indexOf(":"));
}
String elasticsearchUsername = EnvironmentHelper.getElasticsearchServerUsername(configurableEnvironment); String elasticsearchUsername = EnvironmentHelper.getElasticsearchServerUsername(configurableEnvironment);
String elasticsearchPassword = EnvironmentHelper.getElasticsearchServerPassword(configurableEnvironment); String elasticsearchPassword = EnvironmentHelper.getElasticsearchServerPassword(configurableEnvironment);
int elasticsearchPort = Integer.parseInt(elasticsearchUrl.substring(elasticsearchUrl.lastIndexOf(":")+1)); int elasticsearchPort = Integer.parseInt(elasticsearchUrl.substring(elasticsearchUrl.lastIndexOf(":")+1));

View File

@@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.starter; package ca.uhn.fhir.jpa.starter;
import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.jpa.config.HibernateDialectProvider; import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider;
import org.hibernate.dialect.Dialect; import org.hibernate.dialect.Dialect;
import org.hibernate.engine.jdbc.dialect.internal.StandardDialectResolver; import org.hibernate.engine.jdbc.dialect.internal.StandardDialectResolver;
import org.hibernate.engine.jdbc.dialect.spi.DatabaseMetaDataDialectResolutionInfoAdapter; import org.hibernate.engine.jdbc.dialect.spi.DatabaseMetaDataDialectResolutionInfoAdapter;
@@ -10,11 +10,11 @@ import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import javax.sql.DataSource; import javax.sql.DataSource;
import java.sql.SQLException; import java.sql.SQLException;
public class JpaHibernateDialectProvider extends HibernateDialectProvider { public class JpaHibernatePropertiesProvider extends HibernatePropertiesProvider {
private final Dialect dialect; private final Dialect dialect;
public JpaHibernateDialectProvider(LocalContainerEntityManagerFactoryBean myEntityManagerFactory) { public JpaHibernatePropertiesProvider(LocalContainerEntityManagerFactoryBean myEntityManagerFactory) {
DataSource connection = myEntityManagerFactory.getDataSource(); DataSource connection = myEntityManagerFactory.getDataSource();
try { try {
dialect = new StandardDialectResolver() dialect = new StandardDialectResolver()

View File

@@ -0,0 +1,38 @@
package ca.uhn.fhir.jpa.starter;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.interceptor.validation.IRepositoryValidatingRule;
import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingInterceptor;
import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import java.util.List;
/**
* This class can be customized to enable the {@link ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingInterceptor}
* on this server.
*
* The <code>enable_repository_validating_interceptor</code> property must be enabled in <code>application.yaml</code>
* in order to use this class.
*/
public class RepositoryValidationInterceptorFactory {
@Autowired
private ApplicationContext myApplicationContext;
@Autowired
private FhirContext myFhirContext;
public RepositoryValidatingInterceptor build() {
RepositoryValidatingRuleBuilder ruleBuilder = myApplicationContext.getBean(RepositoryValidatingRuleBuilder.class);
// Customize the ruleBuilder here to have the rules you want! We will give a simple example
// of enabling validation for all Patient resources
ruleBuilder.forResourcesOfType("Patient").requireValidationToDeclaredProfiles();
// Do not customize below this line
List<IRepositoryValidatingRule> rules = ruleBuilder.build();
return new RepositoryValidatingInterceptor(myFhirContext, rules);
}
}

View File

@@ -18,6 +18,7 @@ spring:
# hibernate.cache.use_second_level_cache: false # hibernate.cache.use_second_level_cache: false
# hibernate.cache.use_structured_entries: false # hibernate.cache.use_structured_entries: false
# hibernate.cache.use_minimal_puts: false # hibernate.cache.use_minimal_puts: false
# hibernate.search.enabled: true
# hibernate.search.default.directory_provider: filesystem # hibernate.search.default.directory_provider: filesystem
# hibernate.search.default.indexbase: target/lucenefiles # hibernate.search.default.indexbase: target/lucenefiles
# hibernate.search.lucene_version: lucene_current # hibernate.search.lucene_version: lucene_current
@@ -58,6 +59,7 @@ hapi:
# default_encoding: JSON # default_encoding: JSON
# default_pretty_print: true # default_pretty_print: true
# default_page_size: 20 # default_page_size: 20
# enable_repository_validating_interceptor: false
# enable_index_missing_fields: false # enable_index_missing_fields: false
# enforce_referential_integrity_on_delete: false # enforce_referential_integrity_on_delete: false
# enforce_referential_integrity_on_write: false # enforce_referential_integrity_on_write: false
@@ -135,6 +137,7 @@ hapi:
# enabled: false # enabled: false
# password: SomePassword # password: SomePassword
# required_index_status: YELLOW # required_index_status: YELLOW
# rest_url: 'http://localhost:9200' # rest_url: 'localhost:9200'
# protocol: 'http'
# schema_management_strategy: CREATE # schema_management_strategy: CREATE
# username: SomeUsername # username: SomeUsername

View File

@@ -1,5 +1,6 @@
{ {
"version": "1", "version": "1",
"mdmTypes": ["Patient", "Practitioner"],
"candidateSearchParams": [ "candidateSearchParams": [
{ {
"resourceType": "Patient", "resourceType": "Patient",

View File

@@ -26,11 +26,13 @@ import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
import pl.allegro.tech.embeddedelasticsearch.EmbeddedElastic; import org.testcontainers.elasticsearch.ElasticsearchContainer;
import pl.allegro.tech.embeddedelasticsearch.PopularProperties; import org.testcontainers.junit.jupiter.Container;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import java.io.IOException; import java.io.IOException;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Date; import java.util.Date;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.UUID; import java.util.UUID;
@@ -49,7 +51,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
// Because the port is set randomly, we will set the rest_url using the Initializer. // Because the port is set randomly, we will set the rest_url using the Initializer.
// "elasticsearch.rest_url='http://localhost:9200'", // "elasticsearch.rest_url='http://localhost:9200'",
"elasticsearch.username=SomeUsername", "elasticsearch.username=SomeUsername",
"elasticsearch.password=SomePassword" "elasticsearch.password=SomePassword",
"elasticsearch.protocol=http"
}) })
@ContextConfiguration(initializers = ElasticsearchLastNR4IT.Initializer.class) @ContextConfiguration(initializers = ElasticsearchLastNR4IT.Initializer.class)
public class ElasticsearchLastNR4IT { public class ElasticsearchLastNR4IT {
@@ -57,28 +60,18 @@ public class ElasticsearchLastNR4IT {
private IGenericClient ourClient; private IGenericClient ourClient;
private FhirContext ourCtx; private FhirContext ourCtx;
private static final String ELASTIC_VERSION = "6.5.4"; private static final String ELASTIC_VERSION = "7.10.0";
private static EmbeddedElastic embeddedElastic; private static final String ELASTIC_IMAGE = "docker.elastic.co/elasticsearch/elasticsearch:" + ELASTIC_VERSION;
private static ElasticsearchContainer embeddedElastic;
@Autowired @Autowired
private ElasticsearchSvcImpl myElasticsearchSvc; private ElasticsearchSvcImpl myElasticsearchSvc;
@BeforeAll @BeforeAll
public static void beforeClass() { public static void beforeClass() {
embeddedElastic = new ElasticsearchContainer(ELASTIC_IMAGE).withStartupTimeout(Duration.of(300, ChronoUnit.SECONDS));
embeddedElastic = null; embeddedElastic.start();
try {
embeddedElastic = EmbeddedElastic.builder()
.withElasticVersion(ELASTIC_VERSION)
.withSetting(PopularProperties.TRANSPORT_TCP_PORT, 0)
.withSetting(PopularProperties.HTTP_PORT, 0)
.withSetting(PopularProperties.CLUSTER_NAME, UUID.randomUUID())
.withStartTimeout(60, TimeUnit.SECONDS)
.build()
.start();
} catch (IOException | InterruptedException e) {
throw new ConfigurationException(e);
}
} }
@PreDestroy @PreDestroy
@@ -136,7 +129,7 @@ public class ElasticsearchLastNR4IT {
public void initialize( public void initialize(
ConfigurableApplicationContext configurableApplicationContext) { ConfigurableApplicationContext configurableApplicationContext) {
// Since the port is dynamically generated, replace the URL with one that has the correct port // Since the port is dynamically generated, replace the URL with one that has the correct port
TestPropertyValues.of("elasticsearch.rest_url=http://localhost:" + embeddedElastic.getHttpPort()) TestPropertyValues.of("elasticsearch.rest_url=" + embeddedElastic.getHost() +":" + embeddedElastic.getMappedPort(9200))
.applyTo(configurableApplicationContext.getEnvironment()); .applyTo(configurableApplicationContext.getEnvironment());
} }

View File

@@ -14,10 +14,12 @@ import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient; import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Bundle;
import org.junit.Assert; import org.hl7.fhir.r4.model.Observation;
import org.junit.jupiter.api.AfterEach; import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Subscription;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -27,153 +29,41 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static ca.uhn.fhir.util.TestUtil.waitForSize; import static ca.uhn.fhir.util.TestUtil.waitForSize;
import static java.util.Comparator.comparing;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertNotNull;
@ExtendWith(SpringExtension.class) @ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class, properties = @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class, properties =
{ {
"spring.batch.job.enabled=false", "spring.batch.job.enabled=false",
"spring.datasource.url=jdbc:h2:mem:dbr4", "spring.datasource.url=jdbc:h2:mem:dbr4",
"hapi.fhir.fhir_version=R4", "hapi.fhir.enable_repository_validating_interceptor=true",
"hapi.fhir.cql_enabled=true", "hapi.fhir.fhir_version=r4",
"hapi.fhir.subscription.websocket_enabled=true", "hapi.fhir.subscription.websocket_enabled=true",
//Override is currently required when using Empi as the construction of the Empi beans are ambiguous as they are constructed multiple places. This is evident when running in a spring boot environment "hapi.fhir.mdm_enabled=true",
//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.main.allow-bean-definition-overriding=true"
}) })
public class ExampleServerR4IT implements IServerSupport { public class ExampleServerR4IT {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExampleServerDstu2IT.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExampleServerDstu2IT.class);
private IGenericClient ourClient; private IGenericClient ourClient;
private FhirContext ourCtx; private FhirContext ourCtx;
private String ourServerBaseURL;
@Autowired
DaoRegistry myDaoRegistry;
@LocalServerPort @LocalServerPort
private int port; private int port;
@BeforeEach
void beforeEach() {
ourCtx = FhirContext.forR4();
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
ourCtx.getRestfulClientFactory().setSocketTimeout(1200 * 1000);
ourServerBaseURL = "http://localhost:" + port + "/fhir/";
ourClient = ourCtx.newRestfulGenericClient(ourServerBaseURL);
ourClient.registerInterceptor(new LoggingInterceptor(true));
}
@AfterEach
void afterEach() {
ourLog.info("Finished running a test...");
}
@Test @Test
public void testCQLEXM104EvaluateMeasure() throws IOException { @Order(0)
String measureId = "measure-EXM104-8.2.000";
loadBundle("r4/EXM104/EXM104-8.2.000-bundle.json", ourCtx, ourClient);
// http://localhost:8080/fhir/Measure/measure-EXM104-8.2.000/$evaluate-measure?periodStart=2019-01-01&periodEnd=2019-12-31
Parameters inParams = new Parameters();
inParams.addParameter().setName("periodStart").setValue(new StringType("2019-01-01"));
inParams.addParameter().setName("periodEnd").setValue(new StringType("2019-12-31"));
Parameters outParams = ourClient
.operation()
.onInstance(new IdDt("Measure", measureId))
.named("$evaluate-measure")
.withParameters(inParams)
.cacheControl(new CacheControlDirective().setNoCache(true))
.withAdditionalHeader("Content-Type", "application/json")
.useHttpGet()
.execute();
List<Parameters.ParametersParameterComponent> response = outParams.getParameter();
Assert.assertTrue(!response.isEmpty());
Parameters.ParametersParameterComponent component = response.get(0);
Assert.assertTrue(component.getResource() instanceof MeasureReport);
MeasureReport report = (MeasureReport) component.getResource();
Assert.assertEquals("Measure/"+measureId, report.getMeasure());
}
private Bundle loadBundle(String theLocation, FhirContext theCtx, IGenericClient theClient) throws IOException {
String json = stringFromResource(theLocation);
Bundle bundle = (Bundle) theCtx.newJsonParser().parseResource(json);
Bundle result = (Bundle) theClient.transaction().withBundle(bundle).execute();
return result;
}
@Test
public void testCQLEXM130EvaluateMeasure() throws IOException {
String measureId = "measure-EXM130-7.3.000";
loadBundle("r4/EXM130/EXM130-7.3.000-bundle.json", ourCtx, ourClient);
// http://localhost:8080/fhir/Measure/measure-EXM130-FHIR4-7.2.000/$evaluate-measure?periodStart=2019-01-01&periodEnd=2019-12-31
Parameters inParams = new Parameters();
// inParams.addParameter().setName("measure").setValue(new StringType("Measure/measure-EXM104-8.2.000"));
// inParams.addParameter().setName("patient").setValue(new StringType("Patient/numer-EXM104-FHIR3"));
inParams.addParameter().setName("periodStart").setValue(new StringType("2019-01-01"));
inParams.addParameter().setName("periodEnd").setValue(new StringType("2019-12-31"));
Parameters outParams = ourClient
.operation()
.onInstance(new IdDt("Measure", measureId))
.named("$evaluate-measure")
.withParameters(inParams)
.cacheControl(new CacheControlDirective().setNoCache(true))
.withAdditionalHeader("Content-Type", "application/json")
.useHttpGet()
.execute();
List<Parameters.ParametersParameterComponent> response = outParams.getParameter();
Assert.assertTrue(!response.isEmpty());
Parameters.ParametersParameterComponent component = response.get(0);
Assert.assertTrue(component.getResource() instanceof MeasureReport);
MeasureReport report = (MeasureReport) component.getResource();
Assert.assertEquals("Measure/"+measureId, report.getMeasure());
}
@Test
public void testCQLEXM349EvaluateMeasure() throws IOException {
String measureId = "measure-EXM349-2.10.000";
loadBundle("r4/EXM349/EXM349-2.10.000-bundle.json.manuallyeditedtoremovesupplementaldata", ourCtx, ourClient);
// http://localhost:8080/fhir/Measure/measure-EXM349-2.10.000/$evaluate-measure?periodStart=2019-01-01&periodEnd=2019-12-31
Parameters inParams = new Parameters();
inParams.addParameter().setName("periodStart").setValue(new StringType("2019-01-01"));
inParams.addParameter().setName("periodEnd").setValue(new StringType("2019-12-31"));
Parameters outParams = ourClient
.operation()
.onInstance(new IdDt("Measure", measureId))
.named("$evaluate-measure")
.withParameters(inParams)
.cacheControl(new CacheControlDirective().setNoCache(true))
.withAdditionalHeader("Content-Type", "application/json")
.useHttpGet()
.execute();
List<Parameters.ParametersParameterComponent> response = outParams.getParameter();
Assert.assertTrue(!response.isEmpty());
Parameters.ParametersParameterComponent component = response.get(0);
Assert.assertTrue(component.getResource() instanceof MeasureReport);
MeasureReport report = (MeasureReport) component.getResource();
Assert.assertEquals("Measure/"+measureId, report.getMeasure());
}
//@Test
void testCreateAndRead() { void testCreateAndRead() {
String methodName = "testCreateResourceConditional"; String methodName = "testCreateResourceConditional";
@@ -188,27 +78,26 @@ public class ExampleServerR4IT implements IServerSupport {
Patient pt2 = ourClient.read().resource(Patient.class).withId(id).execute(); Patient pt2 = ourClient.read().resource(Patient.class).withId(id).execute();
assertEquals(methodName, pt2.getName().get(0).getFamily()); assertEquals(methodName, pt2.getName().get(0).getFamily());
// Wait until the message has been processed // Test MDM
await().until(() -> getPeople().size() > 0);
List<Person> persons = getPeople();
// Verify a Person was created that links to our Patient // Wait until the MDM message has been processed
Optional<String> personLinkToCreatedPatient = persons.stream() await().until(() -> getPatients().size(), equalTo(2));
.map(Person::getLink) List<Patient> persons = getPatients();
.flatMap(Collection::stream) Patient goldenRecord = persons.get(0);
.map(Person.PersonLinkComponent::getTarget)
.map(Reference::getReference) // Verify that a golden record Patient was created
.filter(pid -> id.toUnqualifiedVersionless().getValue().equals(pid)) assertNotNull(goldenRecord.getMeta().getTag("http://hapifhir.io/fhir/NamingSystem/mdm-record-status", "GOLDEN_RECORD"));
.findAny();
assertTrue(personLinkToCreatedPatient.isPresent());
} }
private List<Person> getPeople() { private List<Patient> getPatients() {
Bundle bundle = ourClient.search().forResource(Person.class).cacheControl(new CacheControlDirective().setNoCache(true)).returnBundle(Bundle.class).execute(); Bundle bundle = ourClient.search().forResource(Patient.class).cacheControl(new CacheControlDirective().setNoCache(true)).returnBundle(Bundle.class).execute();
return BundleUtil.toListOfResourcesOfType(ourCtx, bundle, Person.class); List<Patient> retVal = BundleUtil.toListOfResourcesOfType(ourCtx, bundle, Patient.class);
retVal.sort(comparing(o -> ((Patient) o).getMeta().getLastUpdated()).reversed());
return retVal;
} }
//@Test @Test
@Order(1)
public void testWebsocketSubscription() throws Exception { public void testWebsocketSubscription() throws Exception {
/* /*
* Create subscription * Create subscription
@@ -269,4 +158,16 @@ public class ExampleServerR4IT implements IServerSupport {
private int activeSubscriptionCount() { private int activeSubscriptionCount() {
return ourClient.search().forResource(Subscription.class).where(Subscription.STATUS.exactly().code("active")).cacheControl(new CacheControlDirective().setNoCache(true)).returnBundle(Bundle.class).execute().getEntry().size(); return ourClient.search().forResource(Subscription.class).where(Subscription.STATUS.exactly().code("active")).cacheControl(new CacheControlDirective().setNoCache(true)).returnBundle(Bundle.class).execute().getEntry().size();
} }
@BeforeEach
void beforeEach() {
ourCtx = FhirContext.forR4();
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
ourCtx.getRestfulClientFactory().setSocketTimeout(1200 * 1000);
String ourServerBase = "http://localhost:" + port + "/fhir/";
ourClient = ourCtx.newRestfulGenericClient(ourServerBase);
ourClient.registerInterceptor(new LoggingInterceptor(true));
}
} }

View File

@@ -37,6 +37,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
"spring.batch.job.enabled=false", "spring.batch.job.enabled=false",
"spring.datasource.url=jdbc:h2:mem:dbr5", "spring.datasource.url=jdbc:h2:mem:dbr5",
"hapi.fhir.fhir_version=r5", "hapi.fhir.fhir_version=r5",
"hapi.fhir.subscription.websocket_enabled=true",
"hapi.fhir.subscription.websocket_enabled=true" "hapi.fhir.subscription.websocket_enabled=true"
}) })
public class ExampleServerR5IT { public class ExampleServerR5IT {