Merged the rel_5_3_0 branch into this Branch to pick up the MDM changes.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
[*.java]
|
[*.java]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
indent_style = space
|
indent_style = tab
|
||||||
indent_size = 2
|
indent_size = 3
|
||||||
|
|
||||||
|
|||||||
@@ -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
23
pom.xml
@@ -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>
|
||||||
|
|||||||
45
src/main/java/ca/uhn/fhir/jpa/mdm/MdmConfig.java
Normal file
45
src/main/java/ca/uhn/fhir/jpa/mdm/MdmConfig.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
src/main/java/ca/uhn/fhir/jpa/mdm/MdmConfigCondition.java
Normal file
13
src/main/java/ca/uhn/fhir/jpa/mdm/MdmConfigCondition.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,7 +254,15 @@ public class AppProperties {
|
|||||||
this.enable_index_missing_fields = enable_index_missing_fields;
|
this.enable_index_missing_fields = enable_index_missing_fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getEnforce_referential_integrity_on_delete() {
|
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() {
|
||||||
return enforce_referential_integrity_on_delete;
|
return enforce_referential_integrity_on_delete;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +96,11 @@ public class EnvironmentHelper {
|
|||||||
return environment.getProperty("elasticsearch.rest_url", String.class);
|
return environment.getProperty("elasticsearch.rest_url", String.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getElasticsearchServerUsername(ConfigurableEnvironment environment) {
|
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");
|
return environment.getProperty("elasticsearch.username");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -69,8 +69,14 @@ public class FhirServerConfigR4 extends BaseJavaConfigR4 {
|
|||||||
@Bean()
|
@Bean()
|
||||||
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));
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "1",
|
"version": "1",
|
||||||
|
"mdmTypes": ["Patient", "Practitioner"],
|
||||||
"candidateSearchParams": [
|
"candidateSearchParams": [
|
||||||
{
|
{
|
||||||
"resourceType": "Patient",
|
"resourceType": "Patient",
|
||||||
@@ -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,30 +60,20 @@ 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
|
||||||
public void stop() {
|
public void stop() {
|
||||||
embeddedElastic.stop();
|
embeddedElastic.stop();
|
||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,246 +29,145 @@ 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",
|
||||||
"spring.main.allow-bean-definition-overriding=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"
|
||||||
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
|
@LocalServerPort
|
||||||
DaoRegistry myDaoRegistry;
|
private int port;
|
||||||
|
|
||||||
@LocalServerPort
|
|
||||||
private int port;
|
|
||||||
|
|
||||||
@BeforeEach
|
@Test
|
||||||
void beforeEach() {
|
@Order(0)
|
||||||
ourCtx = FhirContext.forR4();
|
void testCreateAndRead() {
|
||||||
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
|
String methodName = "testCreateResourceConditional";
|
||||||
void afterEach() {
|
|
||||||
ourLog.info("Finished running a test...");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
Patient pt = new Patient();
|
||||||
public void testCQLEXM104EvaluateMeasure() throws IOException {
|
pt.setActive(true);
|
||||||
String measureId = "measure-EXM104-8.2.000";
|
pt.getBirthDateElement().setValueAsString("2020-01-01");
|
||||||
|
pt.addIdentifier().setSystem("http://foo").setValue("12345");
|
||||||
|
pt.addName().setFamily(methodName);
|
||||||
|
IIdType id = ourClient.create().resource(pt).execute().getId();
|
||||||
|
|
||||||
loadBundle("r4/EXM104/EXM104-8.2.000-bundle.json", ourCtx, ourClient);
|
Patient pt2 = ourClient.read().resource(Patient.class).withId(id).execute();
|
||||||
|
assertEquals(methodName, pt2.getName().get(0).getFamily());
|
||||||
|
|
||||||
// http://localhost:8080/fhir/Measure/measure-EXM104-8.2.000/$evaluate-measure?periodStart=2019-01-01&periodEnd=2019-12-31
|
// Test MDM
|
||||||
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
|
// Wait until the MDM message has been processed
|
||||||
.operation()
|
await().until(() -> getPatients().size(), equalTo(2));
|
||||||
.onInstance(new IdDt("Measure", measureId))
|
List<Patient> persons = getPatients();
|
||||||
.named("$evaluate-measure")
|
Patient goldenRecord = persons.get(0);
|
||||||
.withParameters(inParams)
|
|
||||||
.cacheControl(new CacheControlDirective().setNoCache(true))
|
|
||||||
.withAdditionalHeader("Content-Type", "application/json")
|
|
||||||
.useHttpGet()
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
List<Parameters.ParametersParameterComponent> response = outParams.getParameter();
|
// Verify that a golden record Patient was created
|
||||||
Assert.assertTrue(!response.isEmpty());
|
assertNotNull(goldenRecord.getMeta().getTag("http://hapifhir.io/fhir/NamingSystem/mdm-record-status", "GOLDEN_RECORD"));
|
||||||
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 {
|
private List<Patient> getPatients() {
|
||||||
String json = stringFromResource(theLocation);
|
Bundle bundle = ourClient.search().forResource(Patient.class).cacheControl(new CacheControlDirective().setNoCache(true)).returnBundle(Bundle.class).execute();
|
||||||
Bundle bundle = (Bundle) theCtx.newJsonParser().parseResource(json);
|
List<Patient> retVal = BundleUtil.toListOfResourcesOfType(ourCtx, bundle, Patient.class);
|
||||||
Bundle result = (Bundle) theClient.transaction().withBundle(bundle).execute();
|
retVal.sort(comparing(o -> ((Patient) o).getMeta().getLastUpdated()).reversed());
|
||||||
return result;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCQLEXM130EvaluateMeasure() throws IOException {
|
@Order(1)
|
||||||
String measureId = "measure-EXM130-7.3.000";
|
public void testWebsocketSubscription() throws Exception {
|
||||||
|
/*
|
||||||
|
* Create subscription
|
||||||
|
*/
|
||||||
|
Subscription subscription = new Subscription();
|
||||||
|
subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)");
|
||||||
|
subscription.setStatus(Subscription.SubscriptionStatus.REQUESTED);
|
||||||
|
subscription.setCriteria("Observation?status=final");
|
||||||
|
|
||||||
loadBundle("r4/EXM130/EXM130-7.3.000-bundle.json", ourCtx, ourClient);
|
Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent();
|
||||||
|
channel.setType(Subscription.SubscriptionChannelType.WEBSOCKET);
|
||||||
|
channel.setPayload("application/json");
|
||||||
|
subscription.setChannel(channel);
|
||||||
|
|
||||||
// http://localhost:8080/fhir/Measure/measure-EXM130-FHIR4-7.2.000/$evaluate-measure?periodStart=2019-01-01&periodEnd=2019-12-31
|
MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute();
|
||||||
Parameters inParams = new Parameters();
|
IIdType mySubscriptionId = methodOutcome.getId();
|
||||||
// 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
|
// Wait for the subscription to be activated
|
||||||
.operation()
|
await().until(() -> activeSubscriptionCount() == 3);
|
||||||
.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());
|
* Attach websocket
|
||||||
Parameters.ParametersParameterComponent component = response.get(0);
|
*/
|
||||||
Assert.assertTrue(component.getResource() instanceof MeasureReport);
|
|
||||||
MeasureReport report = (MeasureReport) component.getResource();
|
|
||||||
Assert.assertEquals("Measure/"+measureId, report.getMeasure());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
WebSocketClient myWebSocketClient = new WebSocketClient();
|
||||||
public void testCQLEXM349EvaluateMeasure() throws IOException {
|
SocketImplementation mySocketImplementation = new SocketImplementation(mySubscriptionId.getIdPart(), EncodingEnum.JSON);
|
||||||
String measureId = "measure-EXM349-2.10.000";
|
|
||||||
|
|
||||||
loadBundle("r4/EXM349/EXM349-2.10.000-bundle.json.manuallyeditedtoremovesupplementaldata", ourCtx, ourClient);
|
myWebSocketClient.start();
|
||||||
|
URI echoUri = new URI("ws://localhost:" + port + "/websocket");
|
||||||
|
ClientUpgradeRequest request = new ClientUpgradeRequest();
|
||||||
|
ourLog.info("Connecting to : {}", echoUri);
|
||||||
|
Future<Session> connection = myWebSocketClient.connect(mySocketImplementation, echoUri, request);
|
||||||
|
Session session = connection.get(2, TimeUnit.SECONDS);
|
||||||
|
|
||||||
// http://localhost:8080/fhir/Measure/measure-EXM349-2.10.000/$evaluate-measure?periodStart=2019-01-01&periodEnd=2019-12-31
|
ourLog.info("Connected to WS: {}", session.isOpen());
|
||||||
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()
|
* Create a matching resource
|
||||||
.onInstance(new IdDt("Measure", measureId))
|
*/
|
||||||
.named("$evaluate-measure")
|
Observation obs = new Observation();
|
||||||
.withParameters(inParams)
|
obs.setStatus(Observation.ObservationStatus.FINAL);
|
||||||
.cacheControl(new CacheControlDirective().setNoCache(true))
|
ourClient.create().resource(obs).execute();
|
||||||
.withAdditionalHeader("Content-Type", "application/json")
|
|
||||||
.useHttpGet()
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
List<Parameters.ParametersParameterComponent> response = outParams.getParameter();
|
// Give some time for the subscription to deliver
|
||||||
Assert.assertTrue(!response.isEmpty());
|
Thread.sleep(2000);
|
||||||
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() {
|
* Ensure that we receive a ping on the websocket
|
||||||
|
*/
|
||||||
|
waitForSize(1, () -> mySocketImplementation.myPingCount);
|
||||||
|
|
||||||
String methodName = "testCreateResourceConditional";
|
/*
|
||||||
|
* Clean up
|
||||||
|
*/
|
||||||
|
ourClient.delete().resourceById(mySubscriptionId).execute();
|
||||||
|
}
|
||||||
|
|
||||||
Patient pt = new Patient();
|
private int activeSubscriptionCount() {
|
||||||
pt.setActive(true);
|
return ourClient.search().forResource(Subscription.class).where(Subscription.STATUS.exactly().code("active")).cacheControl(new CacheControlDirective().setNoCache(true)).returnBundle(Bundle.class).execute().getEntry().size();
|
||||||
pt.getBirthDateElement().setValueAsString("2020-01-01");
|
}
|
||||||
pt.addIdentifier().setSystem("http://foo").setValue("12345");
|
|
||||||
pt.addName().setFamily(methodName);
|
|
||||||
IIdType id = ourClient.create().resource(pt).execute().getId();
|
|
||||||
|
|
||||||
Patient pt2 = ourClient.read().resource(Patient.class).withId(id).execute();
|
|
||||||
assertEquals(methodName, pt2.getName().get(0).getFamily());
|
|
||||||
|
|
||||||
// Wait until the message has been processed
|
@BeforeEach
|
||||||
await().until(() -> getPeople().size() > 0);
|
void beforeEach() {
|
||||||
List<Person> persons = getPeople();
|
|
||||||
|
|
||||||
// Verify a Person was created that links to our Patient
|
ourCtx = FhirContext.forR4();
|
||||||
Optional<String> personLinkToCreatedPatient = persons.stream()
|
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
|
||||||
.map(Person::getLink)
|
ourCtx.getRestfulClientFactory().setSocketTimeout(1200 * 1000);
|
||||||
.flatMap(Collection::stream)
|
String ourServerBase = "http://localhost:" + port + "/fhir/";
|
||||||
.map(Person.PersonLinkComponent::getTarget)
|
ourClient = ourCtx.newRestfulGenericClient(ourServerBase);
|
||||||
.map(Reference::getReference)
|
ourClient.registerInterceptor(new LoggingInterceptor(true));
|
||||||
.filter(pid -> id.toUnqualifiedVersionless().getValue().equals(pid))
|
}
|
||||||
.findAny();
|
|
||||||
assertTrue(personLinkToCreatedPatient.isPresent());
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Person> getPeople() {
|
|
||||||
Bundle bundle = ourClient.search().forResource(Person.class).cacheControl(new CacheControlDirective().setNoCache(true)).returnBundle(Bundle.class).execute();
|
|
||||||
return BundleUtil.toListOfResourcesOfType(ourCtx, bundle, Person.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
//@Test
|
|
||||||
public void testWebsocketSubscription() throws Exception {
|
|
||||||
/*
|
|
||||||
* Create subscription
|
|
||||||
*/
|
|
||||||
Subscription subscription = new Subscription();
|
|
||||||
subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)");
|
|
||||||
subscription.setStatus(Subscription.SubscriptionStatus.REQUESTED);
|
|
||||||
subscription.setCriteria("Observation?status=final");
|
|
||||||
|
|
||||||
Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent();
|
|
||||||
channel.setType(Subscription.SubscriptionChannelType.WEBSOCKET);
|
|
||||||
channel.setPayload("application/json");
|
|
||||||
subscription.setChannel(channel);
|
|
||||||
|
|
||||||
MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute();
|
|
||||||
IIdType mySubscriptionId = methodOutcome.getId();
|
|
||||||
|
|
||||||
// Wait for the subscription to be activated
|
|
||||||
await().until(() -> activeSubscriptionCount() == 3);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Attach websocket
|
|
||||||
*/
|
|
||||||
|
|
||||||
WebSocketClient myWebSocketClient = new WebSocketClient();
|
|
||||||
SocketImplementation mySocketImplementation = new SocketImplementation(mySubscriptionId.getIdPart(), EncodingEnum.JSON);
|
|
||||||
|
|
||||||
myWebSocketClient.start();
|
|
||||||
URI echoUri = new URI("ws://localhost:" + port + "/websocket");
|
|
||||||
ClientUpgradeRequest request = new ClientUpgradeRequest();
|
|
||||||
ourLog.info("Connecting to : {}", echoUri);
|
|
||||||
Future<Session> connection = myWebSocketClient.connect(mySocketImplementation, echoUri, request);
|
|
||||||
Session session = connection.get(2, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
ourLog.info("Connected to WS: {}", session.isOpen());
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Create a matching resource
|
|
||||||
*/
|
|
||||||
Observation obs = new Observation();
|
|
||||||
obs.setStatus(Observation.ObservationStatus.FINAL);
|
|
||||||
ourClient.create().resource(obs).execute();
|
|
||||||
|
|
||||||
// Give some time for the subscription to deliver
|
|
||||||
Thread.sleep(2000);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Ensure that we receive a ping on the websocket
|
|
||||||
*/
|
|
||||||
waitForSize(1, () -> mySocketImplementation.myPingCount);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Clean up
|
|
||||||
*/
|
|
||||||
ourClient.delete().resourceById(mySubscriptionId).execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ 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 {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user