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]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
indent_style = tab
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
elasticsearch.enabled=true
|
||||
elasticsearch.rest_url=http://localhost:9200
|
||||
elasticsearch.rest_url=localhost:9200
|
||||
elasticsearch.username=SomeUsername
|
||||
elasticsearch.password=SomePassword
|
||||
elasticsearch.protocol=http
|
||||
elasticsearch.required_index_status=YELLOW
|
||||
elasticsearch.schema_management_strategy=CREATE
|
||||
```
|
||||
|
||||
23
pom.xml
23
pom.xml
@@ -22,7 +22,6 @@
|
||||
|
||||
<properties>
|
||||
<java.version>8</java.version>
|
||||
<spring_boot_version>2.3.4.RELEASE</spring_boot_version>
|
||||
</properties>
|
||||
|
||||
<prerequisites>
|
||||
@@ -107,6 +106,12 @@
|
||||
<artifactId>hapi-fhir-jpaserver-cql</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</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 -->
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
@@ -235,6 +240,21 @@
|
||||
<artifactId>jetty-webapp</artifactId>
|
||||
<scope>test</scope>
|
||||
</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>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<configuration>
|
||||
<redirectTestOutputToFile>true</redirectTestOutputToFile>
|
||||
</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 {
|
||||
|
||||
private Boolean cql_enabled = false;
|
||||
private Boolean mdm_enabled = false;
|
||||
private Boolean allow_cascading_deletes = false;
|
||||
private Boolean allow_contains_searches = true;
|
||||
private Boolean allow_external_references = false;
|
||||
@@ -31,6 +32,7 @@ public class AppProperties {
|
||||
private Boolean allow_placeholder_references = true;
|
||||
private Boolean auto_create_placeholder_reference_targets = true;
|
||||
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_write = true;
|
||||
private Boolean etag_support_enabled = true;
|
||||
@@ -97,6 +99,14 @@ public class AppProperties {
|
||||
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() {
|
||||
return cors;
|
||||
}
|
||||
@@ -244,6 +254,14 @@ public class AppProperties {
|
||||
this.enable_index_missing_fields = enable_index_missing_fields;
|
||||
}
|
||||
|
||||
public Boolean getEnable_repository_validating_interceptor() {
|
||||
return enable_repository_validating_interceptor;
|
||||
}
|
||||
|
||||
public void setEnable_repository_validating_interceptor(Boolean theEnable_repository_validating_interceptor) {
|
||||
enable_repository_validating_interceptor = theEnable_repository_validating_interceptor;
|
||||
}
|
||||
|
||||
public Boolean getEnforce_referential_integrity_on_delete() {
|
||||
return enforce_referential_integrity_on_delete;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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.subscription.channel.config.SubscriptionChannelConfig;
|
||||
import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig;
|
||||
@@ -23,7 +24,7 @@ import org.springframework.web.servlet.DispatcherServlet;
|
||||
@ServletComponentScan(basePackageClasses = {
|
||||
JpaRestfulServer.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 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.bulk.provider.BulkDataExportProvider;
|
||||
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.PackageInstallationSpec;
|
||||
import ca.uhn.fhir.jpa.partition.PartitionManagementProvider;
|
||||
@@ -365,6 +366,12 @@ public class BaseJpaRestfulServer extends RestfulServer {
|
||||
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;
|
||||
|
||||
import ca.uhn.fhir.jpa.search.HapiLuceneAnalysisConfigurer;
|
||||
import ca.uhn.fhir.jpa.search.elastic.ElasticsearchHibernatePropertiesBuilder;
|
||||
import org.hibernate.search.elasticsearch.cfg.ElasticsearchIndexStatus;
|
||||
import org.hibernate.search.elasticsearch.cfg.IndexSchemaManagementStrategy;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.EnumerablePropertySource;
|
||||
@@ -19,7 +27,6 @@ public class EnvironmentHelper {
|
||||
Properties properties = new Properties();
|
||||
|
||||
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.show_sql", "false");
|
||||
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_structured_entries", "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("hibernate.search.lucene_version", "LUCENE_CURRENT");
|
||||
|
||||
properties.put(BackendSettings.backendKey(LuceneIndexSettings.DIRECTORY_TYPE), "local-filesystem");
|
||||
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 {
|
||||
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("=");
|
||||
properties.put(values[0], values[1]);
|
||||
@@ -42,9 +56,9 @@ public class EnvironmentHelper {
|
||||
if (environment.getProperty("elasticsearch.enabled", Boolean.class) != null
|
||||
&& environment.getProperty("elasticsearch.enabled", Boolean.class) == true ){
|
||||
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) {
|
||||
builder.setRequiredIndexStatus(ElasticsearchIndexStatus.YELLOW);
|
||||
builder.setRequiredIndexStatus(IndexStatus.YELLOW);
|
||||
} else {
|
||||
builder.setRequiredIndexStatus(requiredIndexStatus);
|
||||
}
|
||||
@@ -52,18 +66,19 @@ public class EnvironmentHelper {
|
||||
builder.setRestUrl(getElasticsearchServerUrl(environment));
|
||||
builder.setUsername(getElasticsearchServerUsername(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) {
|
||||
builder.setIndexSchemaManagementStrategy(IndexSchemaManagementStrategy.CREATE);
|
||||
builder.setIndexSchemaManagementStrategy(SchemaManagementStrategyName.CREATE);
|
||||
} else {
|
||||
builder.setIndexSchemaManagementStrategy(indexSchemaManagementStrategy);
|
||||
}
|
||||
// pretty_print_json_log: false
|
||||
Boolean refreshAfterWrite = environment.getProperty("elasticsearch.debug.refresh_after_write", Boolean.class);
|
||||
if (refreshAfterWrite == null) {
|
||||
builder.setDebugRefreshAfterWrite(false);
|
||||
if (refreshAfterWrite == null || refreshAfterWrite == false) {
|
||||
builder.setDebugIndexSyncStrategy(AutomaticIndexingSynchronizationStrategyNames.ASYNC);
|
||||
} else {
|
||||
builder.setDebugRefreshAfterWrite(refreshAfterWrite);
|
||||
builder.setDebugIndexSyncStrategy(AutomaticIndexingSynchronizationStrategyNames.READ_SYNC);
|
||||
}
|
||||
// pretty_print_json_log: false
|
||||
Boolean prettyPrintJsonLog = environment.getProperty("elasticsearch.debug.pretty_print_json_log", Boolean.class);
|
||||
@@ -74,7 +89,6 @@ public class EnvironmentHelper {
|
||||
}
|
||||
builder.apply(properties);
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
@@ -82,6 +96,10 @@ public class EnvironmentHelper {
|
||||
return environment.getProperty("elasticsearch.rest_url", String.class);
|
||||
}
|
||||
|
||||
public static String getElasticsearchServerProtocol(ConfigurableEnvironment environment) {
|
||||
return environment.getProperty("elasticsearch.protocol", String.class, "http");
|
||||
}
|
||||
|
||||
public static String getElasticsearchServerUsername(ConfigurableEnvironment environment) {
|
||||
return environment.getProperty("elasticsearch.username");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package ca.uhn.fhir.jpa.starter;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.binstore.DatabaseBlobBinaryStorageSvcImpl;
|
||||
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.entity.ModelConfig;
|
||||
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
|
||||
@Bean
|
||||
public HibernateDialectProvider jpaStarterDialectProvider(LocalContainerEntityManagerFactoryBean myEntityManagerFactory) {
|
||||
return new JpaHibernateDialectProvider(myEntityManagerFactory);
|
||||
public HibernatePropertiesProvider jpaStarterDialectProvider(LocalContainerEntityManagerFactoryBean myEntityManagerFactory) {
|
||||
return new JpaHibernatePropertiesProvider(myEntityManagerFactory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -70,7 +70,13 @@ public class FhirServerConfigR4 extends BaseJavaConfigR4 {
|
||||
public ElasticsearchSvcImpl elasticsearchSvc() {
|
||||
if (EnvironmentHelper.isElasticsearchEnabled(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 elasticsearchPassword = EnvironmentHelper.getElasticsearchServerPassword(configurableEnvironment);
|
||||
int elasticsearchPort = Integer.parseInt(elasticsearchUrl.substring(elasticsearchUrl.lastIndexOf(":")+1));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ca.uhn.fhir.jpa.starter;
|
||||
|
||||
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.engine.jdbc.dialect.internal.StandardDialectResolver;
|
||||
import org.hibernate.engine.jdbc.dialect.spi.DatabaseMetaDataDialectResolutionInfoAdapter;
|
||||
@@ -10,11 +10,11 @@ import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class JpaHibernateDialectProvider extends HibernateDialectProvider {
|
||||
public class JpaHibernatePropertiesProvider extends HibernatePropertiesProvider {
|
||||
|
||||
private final Dialect dialect;
|
||||
|
||||
public JpaHibernateDialectProvider(LocalContainerEntityManagerFactoryBean myEntityManagerFactory) {
|
||||
public JpaHibernatePropertiesProvider(LocalContainerEntityManagerFactoryBean myEntityManagerFactory) {
|
||||
DataSource connection = myEntityManagerFactory.getDataSource();
|
||||
try {
|
||||
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_structured_entries: false
|
||||
# hibernate.cache.use_minimal_puts: false
|
||||
# hibernate.search.enabled: true
|
||||
# hibernate.search.default.directory_provider: filesystem
|
||||
# hibernate.search.default.indexbase: target/lucenefiles
|
||||
# hibernate.search.lucene_version: lucene_current
|
||||
@@ -58,6 +59,7 @@ hapi:
|
||||
# default_encoding: JSON
|
||||
# default_pretty_print: true
|
||||
# default_page_size: 20
|
||||
# enable_repository_validating_interceptor: false
|
||||
# enable_index_missing_fields: false
|
||||
# enforce_referential_integrity_on_delete: false
|
||||
# enforce_referential_integrity_on_write: false
|
||||
@@ -135,6 +137,7 @@ hapi:
|
||||
# enabled: false
|
||||
# password: SomePassword
|
||||
# required_index_status: YELLOW
|
||||
# rest_url: 'http://localhost:9200'
|
||||
# rest_url: 'localhost:9200'
|
||||
# protocol: 'http'
|
||||
# schema_management_strategy: CREATE
|
||||
# username: SomeUsername
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"version": "1",
|
||||
"mdmTypes": ["Patient", "Practitioner"],
|
||||
"candidateSearchParams": [
|
||||
{
|
||||
"resourceType": "Patient",
|
||||
@@ -26,11 +26,13 @@ import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import pl.allegro.tech.embeddedelasticsearch.EmbeddedElastic;
|
||||
import pl.allegro.tech.embeddedelasticsearch.PopularProperties;
|
||||
import org.testcontainers.elasticsearch.ElasticsearchContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
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.
|
||||
// "elasticsearch.rest_url='http://localhost:9200'",
|
||||
"elasticsearch.username=SomeUsername",
|
||||
"elasticsearch.password=SomePassword"
|
||||
"elasticsearch.password=SomePassword",
|
||||
"elasticsearch.protocol=http"
|
||||
})
|
||||
@ContextConfiguration(initializers = ElasticsearchLastNR4IT.Initializer.class)
|
||||
public class ElasticsearchLastNR4IT {
|
||||
@@ -57,28 +60,18 @@ public class ElasticsearchLastNR4IT {
|
||||
private IGenericClient ourClient;
|
||||
private FhirContext ourCtx;
|
||||
|
||||
private static final String ELASTIC_VERSION = "6.5.4";
|
||||
private static EmbeddedElastic embeddedElastic;
|
||||
private static final String ELASTIC_VERSION = "7.10.0";
|
||||
private static final String ELASTIC_IMAGE = "docker.elastic.co/elasticsearch/elasticsearch:" + ELASTIC_VERSION;
|
||||
|
||||
private static ElasticsearchContainer embeddedElastic;
|
||||
|
||||
@Autowired
|
||||
private ElasticsearchSvcImpl myElasticsearchSvc;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeClass() {
|
||||
|
||||
embeddedElastic = null;
|
||||
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);
|
||||
}
|
||||
embeddedElastic = new ElasticsearchContainer(ELASTIC_IMAGE).withStartupTimeout(Duration.of(300, ChronoUnit.SECONDS));
|
||||
embeddedElastic.start();
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
@@ -136,7 +129,7 @@ public class ElasticsearchLastNR4IT {
|
||||
public void initialize(
|
||||
ConfigurableApplicationContext configurableApplicationContext) {
|
||||
// Since the port is dynamically generated, replace the URL with one that has the correct port
|
||||
TestPropertyValues.of("elasticsearch.rest_url=http://localhost:" + embeddedElastic.getHttpPort())
|
||||
TestPropertyValues.of("elasticsearch.rest_url=" + embeddedElastic.getHost() +":" + embeddedElastic.getMappedPort(9200))
|
||||
.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.WebSocketClient;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.*;
|
||||
import org.junit.Assert;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
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.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -27,153 +29,41 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static ca.uhn.fhir.util.TestUtil.waitForSize;
|
||||
import static java.util.Comparator.comparing;
|
||||
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.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class, properties =
|
||||
{
|
||||
"spring.batch.job.enabled=false",
|
||||
"spring.datasource.url=jdbc:h2:mem:dbr4",
|
||||
"hapi.fhir.fhir_version=R4",
|
||||
"hapi.fhir.cql_enabled=true",
|
||||
"hapi.fhir.enable_repository_validating_interceptor=true",
|
||||
"hapi.fhir.fhir_version=r4",
|
||||
"hapi.fhir.subscription.websocket_enabled=true",
|
||||
//Override is currently required when using Empi as the construction of the Empi beans are ambiguous as they are constructed multiple places. This is evident when running in a spring boot environment
|
||||
"hapi.fhir.mdm_enabled=true",
|
||||
//Override is currently required when using MDM as the construction of the MDM beans are ambiguous as they are constructed multiple places. This is evident when running in a spring boot environment
|
||||
"spring.main.allow-bean-definition-overriding=true"
|
||||
})
|
||||
public class ExampleServerR4IT implements IServerSupport {
|
||||
public class ExampleServerR4IT {
|
||||
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExampleServerDstu2IT.class);
|
||||
private IGenericClient ourClient;
|
||||
private FhirContext ourCtx;
|
||||
private String ourServerBaseURL;
|
||||
|
||||
@Autowired
|
||||
DaoRegistry myDaoRegistry;
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
ourCtx = FhirContext.forR4();
|
||||
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
|
||||
ourCtx.getRestfulClientFactory().setSocketTimeout(1200 * 1000);
|
||||
ourServerBaseURL = "http://localhost:" + port + "/fhir/";
|
||||
ourClient = ourCtx.newRestfulGenericClient(ourServerBaseURL);
|
||||
ourClient.registerInterceptor(new LoggingInterceptor(true));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() {
|
||||
ourLog.info("Finished running a test...");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCQLEXM104EvaluateMeasure() throws IOException {
|
||||
String measureId = "measure-EXM104-8.2.000";
|
||||
|
||||
loadBundle("r4/EXM104/EXM104-8.2.000-bundle.json", ourCtx, ourClient);
|
||||
|
||||
// http://localhost:8080/fhir/Measure/measure-EXM104-8.2.000/$evaluate-measure?periodStart=2019-01-01&periodEnd=2019-12-31
|
||||
Parameters inParams = new Parameters();
|
||||
inParams.addParameter().setName("periodStart").setValue(new StringType("2019-01-01"));
|
||||
inParams.addParameter().setName("periodEnd").setValue(new StringType("2019-12-31"));
|
||||
|
||||
Parameters outParams = ourClient
|
||||
.operation()
|
||||
.onInstance(new IdDt("Measure", measureId))
|
||||
.named("$evaluate-measure")
|
||||
.withParameters(inParams)
|
||||
.cacheControl(new CacheControlDirective().setNoCache(true))
|
||||
.withAdditionalHeader("Content-Type", "application/json")
|
||||
.useHttpGet()
|
||||
.execute();
|
||||
|
||||
List<Parameters.ParametersParameterComponent> response = outParams.getParameter();
|
||||
Assert.assertTrue(!response.isEmpty());
|
||||
Parameters.ParametersParameterComponent component = response.get(0);
|
||||
Assert.assertTrue(component.getResource() instanceof MeasureReport);
|
||||
MeasureReport report = (MeasureReport) component.getResource();
|
||||
Assert.assertEquals("Measure/"+measureId, report.getMeasure());
|
||||
}
|
||||
|
||||
private Bundle loadBundle(String theLocation, FhirContext theCtx, IGenericClient theClient) throws IOException {
|
||||
String json = stringFromResource(theLocation);
|
||||
Bundle bundle = (Bundle) theCtx.newJsonParser().parseResource(json);
|
||||
Bundle result = (Bundle) theClient.transaction().withBundle(bundle).execute();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCQLEXM130EvaluateMeasure() throws IOException {
|
||||
String measureId = "measure-EXM130-7.3.000";
|
||||
|
||||
loadBundle("r4/EXM130/EXM130-7.3.000-bundle.json", ourCtx, ourClient);
|
||||
|
||||
// http://localhost:8080/fhir/Measure/measure-EXM130-FHIR4-7.2.000/$evaluate-measure?periodStart=2019-01-01&periodEnd=2019-12-31
|
||||
Parameters inParams = new Parameters();
|
||||
// inParams.addParameter().setName("measure").setValue(new StringType("Measure/measure-EXM104-8.2.000"));
|
||||
// inParams.addParameter().setName("patient").setValue(new StringType("Patient/numer-EXM104-FHIR3"));
|
||||
inParams.addParameter().setName("periodStart").setValue(new StringType("2019-01-01"));
|
||||
inParams.addParameter().setName("periodEnd").setValue(new StringType("2019-12-31"));
|
||||
|
||||
Parameters outParams = ourClient
|
||||
.operation()
|
||||
.onInstance(new IdDt("Measure", measureId))
|
||||
.named("$evaluate-measure")
|
||||
.withParameters(inParams)
|
||||
.cacheControl(new CacheControlDirective().setNoCache(true))
|
||||
.withAdditionalHeader("Content-Type", "application/json")
|
||||
.useHttpGet()
|
||||
.execute();
|
||||
|
||||
List<Parameters.ParametersParameterComponent> response = outParams.getParameter();
|
||||
Assert.assertTrue(!response.isEmpty());
|
||||
Parameters.ParametersParameterComponent component = response.get(0);
|
||||
Assert.assertTrue(component.getResource() instanceof MeasureReport);
|
||||
MeasureReport report = (MeasureReport) component.getResource();
|
||||
Assert.assertEquals("Measure/"+measureId, report.getMeasure());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCQLEXM349EvaluateMeasure() throws IOException {
|
||||
String measureId = "measure-EXM349-2.10.000";
|
||||
|
||||
loadBundle("r4/EXM349/EXM349-2.10.000-bundle.json.manuallyeditedtoremovesupplementaldata", ourCtx, ourClient);
|
||||
|
||||
// http://localhost:8080/fhir/Measure/measure-EXM349-2.10.000/$evaluate-measure?periodStart=2019-01-01&periodEnd=2019-12-31
|
||||
Parameters inParams = new Parameters();
|
||||
inParams.addParameter().setName("periodStart").setValue(new StringType("2019-01-01"));
|
||||
inParams.addParameter().setName("periodEnd").setValue(new StringType("2019-12-31"));
|
||||
|
||||
Parameters outParams = ourClient
|
||||
.operation()
|
||||
.onInstance(new IdDt("Measure", measureId))
|
||||
.named("$evaluate-measure")
|
||||
.withParameters(inParams)
|
||||
.cacheControl(new CacheControlDirective().setNoCache(true))
|
||||
.withAdditionalHeader("Content-Type", "application/json")
|
||||
.useHttpGet()
|
||||
.execute();
|
||||
|
||||
List<Parameters.ParametersParameterComponent> response = outParams.getParameter();
|
||||
Assert.assertTrue(!response.isEmpty());
|
||||
Parameters.ParametersParameterComponent component = response.get(0);
|
||||
Assert.assertTrue(component.getResource() instanceof MeasureReport);
|
||||
MeasureReport report = (MeasureReport) component.getResource();
|
||||
Assert.assertEquals("Measure/"+measureId, report.getMeasure());
|
||||
}
|
||||
|
||||
//@Test
|
||||
@Order(0)
|
||||
void testCreateAndRead() {
|
||||
|
||||
String methodName = "testCreateResourceConditional";
|
||||
@@ -188,27 +78,26 @@ public class ExampleServerR4IT implements IServerSupport {
|
||||
Patient pt2 = ourClient.read().resource(Patient.class).withId(id).execute();
|
||||
assertEquals(methodName, pt2.getName().get(0).getFamily());
|
||||
|
||||
// Wait until the message has been processed
|
||||
await().until(() -> getPeople().size() > 0);
|
||||
List<Person> persons = getPeople();
|
||||
// Test MDM
|
||||
|
||||
// Verify a Person was created that links to our Patient
|
||||
Optional<String> personLinkToCreatedPatient = persons.stream()
|
||||
.map(Person::getLink)
|
||||
.flatMap(Collection::stream)
|
||||
.map(Person.PersonLinkComponent::getTarget)
|
||||
.map(Reference::getReference)
|
||||
.filter(pid -> id.toUnqualifiedVersionless().getValue().equals(pid))
|
||||
.findAny();
|
||||
assertTrue(personLinkToCreatedPatient.isPresent());
|
||||
// Wait until the MDM message has been processed
|
||||
await().until(() -> getPatients().size(), equalTo(2));
|
||||
List<Patient> persons = getPatients();
|
||||
Patient goldenRecord = persons.get(0);
|
||||
|
||||
// Verify that a golden record Patient was created
|
||||
assertNotNull(goldenRecord.getMeta().getTag("http://hapifhir.io/fhir/NamingSystem/mdm-record-status", "GOLDEN_RECORD"));
|
||||
}
|
||||
|
||||
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);
|
||||
private List<Patient> getPatients() {
|
||||
Bundle bundle = ourClient.search().forResource(Patient.class).cacheControl(new CacheControlDirective().setNoCache(true)).returnBundle(Bundle.class).execute();
|
||||
List<Patient> retVal = BundleUtil.toListOfResourcesOfType(ourCtx, bundle, Patient.class);
|
||||
retVal.sort(comparing(o -> ((Patient) o).getMeta().getLastUpdated()).reversed());
|
||||
return retVal;
|
||||
}
|
||||
|
||||
//@Test
|
||||
@Test
|
||||
@Order(1)
|
||||
public void testWebsocketSubscription() throws Exception {
|
||||
/*
|
||||
* Create subscription
|
||||
@@ -269,4 +158,16 @@ public class ExampleServerR4IT implements IServerSupport {
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
|
||||
ourCtx = FhirContext.forR4();
|
||||
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
|
||||
ourCtx.getRestfulClientFactory().setSocketTimeout(1200 * 1000);
|
||||
String ourServerBase = "http://localhost:" + port + "/fhir/";
|
||||
ourClient = ourCtx.newRestfulGenericClient(ourServerBase);
|
||||
ourClient.registerInterceptor(new LoggingInterceptor(true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
"spring.batch.job.enabled=false",
|
||||
"spring.datasource.url=jdbc:h2:mem:dbr5",
|
||||
"hapi.fhir.fhir_version=r5",
|
||||
"hapi.fhir.subscription.websocket_enabled=true",
|
||||
"hapi.fhir.subscription.websocket_enabled=true"
|
||||
})
|
||||
public class ExampleServerR5IT {
|
||||
|
||||
Reference in New Issue
Block a user