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,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;
} }

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,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");
} }

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

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

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,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());
} }

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,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();
}
} }

View File

@@ -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 {