Merge remote-tracking branch 'origin/master' into rel_8_7-tracking
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -167,3 +167,4 @@ Temporary Items
|
|||||||
|
|
||||||
# Helm Chart dependencies
|
# Helm Chart dependencies
|
||||||
**/charts/*.tgz
|
**/charts/*.tgz
|
||||||
|
.claude
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -73,13 +73,25 @@ docker run -p 8090:8080 -v $(pwd)/yourLocalFolder:/configs -e "--spring.config.l
|
|||||||
```
|
```
|
||||||
Here, the configuration file (*another.application.yaml*) is placed locally in the folder *yourLocalFolder*.
|
Here, the configuration file (*another.application.yaml*) is placed locally in the folder *yourLocalFolder*.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run -p 8090:8080 -e "--spring.config.location=classpath:/another.application.yaml" hapiproject/hapi:latest
|
docker run -p 8090:8080 -e "--spring.config.location=classpath:/another.application.yaml" hapiproject/hapi:latest
|
||||||
```
|
```
|
||||||
Here, the configuration file (*another.application.yaml*) is part of the compiled set of resources.
|
Here, the configuration file (*another.application.yaml*) is part of the compiled set of resources.
|
||||||
|
|
||||||
|
### Configuration with additional override files
|
||||||
|
|
||||||
|
You can layer additional configuration files on top of the default application.yaml while preserving all the base settings. This approach allows you to create specific override files for different environments without duplicating the entire configuration.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Maven
|
||||||
|
mvn spring-boot:run -Dspring-boot.run.arguments="--spring.config.additional-location=classpath:your-overrides.yaml"
|
||||||
|
|
||||||
|
# Using Docker
|
||||||
|
docker run -p 8080:8080 -e "--spring.config.additional-location=classpath:your-overrides.yaml" hapiproject/hapi:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, the additional configuration file (*your-overrides.yaml*) contains only the specific properties you want to override or add, while all default values from application.yaml remain unchanged.
|
||||||
|
|
||||||
### One-liner for quickly getting an Implementation Guide installed into HAPI
|
### One-liner for quickly getting an Implementation Guide installed into HAPI
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
26
pom.xml
26
pom.xml
@@ -42,10 +42,6 @@
|
|||||||
<version>${project.parent.version}-${hapi.fhir.jpa.server.starter.revision}</version>
|
<version>${project.parent.version}-${hapi.fhir.jpa.server.starter.revision}</version>
|
||||||
<packaging>war</packaging>
|
<packaging>war</packaging>
|
||||||
|
|
||||||
<prerequisites>
|
|
||||||
<maven>3.8.3</maven>
|
|
||||||
</prerequisites>
|
|
||||||
|
|
||||||
<name>HAPI FHIR JPA Server - Starter Project</name>
|
<name>HAPI FHIR JPA Server - Starter Project</name>
|
||||||
|
|
||||||
<repositories>
|
<repositories>
|
||||||
@@ -541,6 +537,28 @@
|
|||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Enforce minimum Maven version requirement -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-enforcer-plugin</artifactId>
|
||||||
|
<version>3.6.2</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>enforce-maven</id>
|
||||||
|
<goals>
|
||||||
|
<goal>enforce</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<rules>
|
||||||
|
<requireMavenVersion>
|
||||||
|
<version>3.8.3</version>
|
||||||
|
</requireMavenVersion>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
<!-- This is to run the integration tests -->
|
<!-- This is to run the integration tests -->
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public class AppProperties {
|
|||||||
|
|
||||||
private BinaryStorageMode binary_storage_mode = BinaryStorageMode.DATABASE;
|
private BinaryStorageMode binary_storage_mode = BinaryStorageMode.DATABASE;
|
||||||
private String binary_storage_filesystem_base_directory;
|
private String binary_storage_filesystem_base_directory;
|
||||||
private Integer inline_resource_storage_below_size;
|
private Integer binary_storage_minimum_binary_size;
|
||||||
private Boolean bulk_export_enabled = false;
|
private Boolean bulk_export_enabled = false;
|
||||||
private Boolean bulk_import_enabled = false;
|
private Boolean bulk_import_enabled = false;
|
||||||
private Boolean default_pretty_print = true;
|
private Boolean default_pretty_print = true;
|
||||||
@@ -131,6 +131,8 @@ public class AppProperties {
|
|||||||
private Integer expunge_thread_count = null;
|
private Integer expunge_thread_count = null;
|
||||||
private Elasticsearch elasticsearch = null;
|
private Elasticsearch elasticsearch = null;
|
||||||
|
|
||||||
|
private Integer bulk_export_file_retention_period_hours = 2;
|
||||||
|
|
||||||
public List<String> getCustomInterceptorClasses() {
|
public List<String> getCustomInterceptorClasses() {
|
||||||
return custom_interceptor_classes;
|
return custom_interceptor_classes;
|
||||||
}
|
}
|
||||||
@@ -511,12 +513,12 @@ public class AppProperties {
|
|||||||
this.binary_storage_filesystem_base_directory = binary_storage_filesystem_base_directory;
|
this.binary_storage_filesystem_base_directory = binary_storage_filesystem_base_directory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getInline_resource_storage_below_size() {
|
public Integer getBinary_storage_minimum_binary_size() {
|
||||||
return inline_resource_storage_below_size;
|
return binary_storage_minimum_binary_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setInline_resource_storage_below_size(Integer inline_resource_storage_below_size) {
|
public void setBinary_storage_minimum_binary_size(Integer binary_storage_minimum_binary_size) {
|
||||||
this.inline_resource_storage_below_size = inline_resource_storage_below_size;
|
this.binary_storage_minimum_binary_size = binary_storage_minimum_binary_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getBulk_export_enabled() {
|
public Boolean getBulk_export_enabled() {
|
||||||
@@ -856,6 +858,14 @@ public class AppProperties {
|
|||||||
this.elasticsearch = elasticsearch;
|
this.elasticsearch = elasticsearch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getBulk_export_file_retention_period_hours() {
|
||||||
|
return bulk_export_file_retention_period_hours;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBulk_export_file_retention_period_hours(Integer bulk_export_file_retention_period_hours) {
|
||||||
|
this.bulk_export_file_retention_period_hours = bulk_export_file_retention_period_hours;
|
||||||
|
}
|
||||||
|
|
||||||
public static class Cors {
|
public static class Cors {
|
||||||
private Boolean allow_Credentials = true;
|
private Boolean allow_Credentials = true;
|
||||||
private List<String> allowed_origin = List.of("*");
|
private List<String> allowed_origin = List.of("*");
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package ca.uhn.fhir.jpa.starter.common;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.IHapiBootOrder;
|
||||||
|
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||||
|
import ca.uhn.fhir.jpa.binary.interceptor.BinaryStorageInterceptor;
|
||||||
|
import ca.uhn.fhir.jpa.starter.AppProperties;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.context.event.ContextRefreshedEvent;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class BinaryStorageInterceptorRegistrar {
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(BinaryStorageInterceptorRegistrar.class);
|
||||||
|
|
||||||
|
private final IInterceptorService myInterceptorService;
|
||||||
|
private final BinaryStorageInterceptor<?> myBinaryStorageInterceptor;
|
||||||
|
private final AppProperties myAppProperties;
|
||||||
|
|
||||||
|
public BinaryStorageInterceptorRegistrar(
|
||||||
|
IInterceptorService theInterceptorService,
|
||||||
|
BinaryStorageInterceptor<?> theBinaryStorageInterceptor,
|
||||||
|
AppProperties theAppProperties) {
|
||||||
|
myInterceptorService = theInterceptorService;
|
||||||
|
myBinaryStorageInterceptor = theBinaryStorageInterceptor;
|
||||||
|
myAppProperties = theAppProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener(classes = {ContextRefreshedEvent.class})
|
||||||
|
@Order(IHapiBootOrder.REGISTER_INTERCEPTORS)
|
||||||
|
public void register() {
|
||||||
|
if (!myAppProperties.getBinary_storage_enabled()) {
|
||||||
|
ourLog.debug("Binary storage disabled; skipping BinaryStorageInterceptor registration");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ourLog.info("Registering BinaryStorageInterceptor with JPA interceptor service");
|
||||||
|
myInterceptorService.registerInterceptor(myBinaryStorageInterceptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -227,11 +227,6 @@ public class FhirServerConfigCommon {
|
|||||||
jpaStorageSettings.setLastNEnabled(true);
|
jpaStorageSettings.setLastNEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Integer inlineResourceThreshold = resolveInlineResourceThreshold(appProperties);
|
|
||||||
if (inlineResourceThreshold != null && inlineResourceThreshold != 0) {
|
|
||||||
jpaStorageSettings.setInlineResourceTextBelowSize(inlineResourceThreshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
jpaStorageSettings.setStoreResourceInHSearchIndex(appProperties.getStore_resource_in_lucene_index_enabled());
|
jpaStorageSettings.setStoreResourceInHSearchIndex(appProperties.getStore_resource_in_lucene_index_enabled());
|
||||||
jpaStorageSettings.setNormalizedQuantitySearchLevel(appProperties.getNormalized_quantity_search_level());
|
jpaStorageSettings.setNormalizedQuantitySearchLevel(appProperties.getNormalized_quantity_search_level());
|
||||||
jpaStorageSettings.setIndexOnContainedResources(appProperties.getEnable_index_contained_resource());
|
jpaStorageSettings.setIndexOnContainedResources(appProperties.getEnable_index_contained_resource());
|
||||||
@@ -302,6 +297,12 @@ public class FhirServerConfigCommon {
|
|||||||
jpaStorageSettings.setHSearchIndexPrefix(indexPrefix != null ? indexPrefix : "");
|
jpaStorageSettings.setHSearchIndexPrefix(indexPrefix != null ? indexPrefix : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configure the bulk export file retention period
|
||||||
|
if (appProperties.getBulk_export_file_retention_period_hours() != null) {
|
||||||
|
jpaStorageSettings.setBulkExportFileRetentionPeriodHours(
|
||||||
|
appProperties.getBulk_export_file_retention_period_hours());
|
||||||
|
}
|
||||||
|
|
||||||
return jpaStorageSettings;
|
return jpaStorageSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +398,7 @@ public class FhirServerConfigCommon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Integer resolveInlineResourceThreshold(AppProperties appProperties) {
|
private Integer resolveInlineResourceThreshold(AppProperties appProperties) {
|
||||||
Integer inlineResourceThreshold = appProperties.getInline_resource_storage_below_size();
|
Integer inlineResourceThreshold = appProperties.getBinary_storage_minimum_binary_size();
|
||||||
if (inlineResourceThreshold == null
|
if (inlineResourceThreshold == null
|
||||||
&& appProperties.getBinary_storage_mode() == AppProperties.BinaryStorageMode.FILESYSTEM) {
|
&& appProperties.getBinary_storage_mode() == AppProperties.BinaryStorageMode.FILESYSTEM) {
|
||||||
return DEFAULT_FILESYSTEM_INLINE_THRESHOLD;
|
return DEFAULT_FILESYSTEM_INLINE_THRESHOLD;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
|
|||||||
import ca.uhn.fhir.jpa.api.config.ThreadPoolFactoryConfig;
|
import ca.uhn.fhir.jpa.api.config.ThreadPoolFactoryConfig;
|
||||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||||
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
|
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
|
||||||
import ca.uhn.fhir.jpa.binary.interceptor.BinaryStorageInterceptor;
|
|
||||||
import ca.uhn.fhir.jpa.binary.provider.BinaryAccessProvider;
|
import ca.uhn.fhir.jpa.binary.provider.BinaryAccessProvider;
|
||||||
import ca.uhn.fhir.jpa.config.util.HapiEntityManagerFactoryUtil;
|
import ca.uhn.fhir.jpa.config.util.HapiEntityManagerFactoryUtil;
|
||||||
import ca.uhn.fhir.jpa.config.util.ResourceCountCacheUtil;
|
import ca.uhn.fhir.jpa.config.util.ResourceCountCacheUtil;
|
||||||
@@ -50,6 +49,7 @@ import ca.uhn.fhir.jpa.starter.AppProperties;
|
|||||||
import ca.uhn.fhir.jpa.starter.annotations.OnCorsPresent;
|
import ca.uhn.fhir.jpa.starter.annotations.OnCorsPresent;
|
||||||
import ca.uhn.fhir.jpa.starter.annotations.OnImplementationGuidesPresent;
|
import ca.uhn.fhir.jpa.starter.annotations.OnImplementationGuidesPresent;
|
||||||
import ca.uhn.fhir.jpa.starter.common.validation.IRepositoryValidationInterceptorFactory;
|
import ca.uhn.fhir.jpa.starter.common.validation.IRepositoryValidationInterceptorFactory;
|
||||||
|
import ca.uhn.fhir.jpa.starter.elastic.ElasticsearchBootSvcImpl;
|
||||||
import ca.uhn.fhir.jpa.starter.ig.ExtendedPackageInstallationSpec;
|
import ca.uhn.fhir.jpa.starter.ig.ExtendedPackageInstallationSpec;
|
||||||
import ca.uhn.fhir.jpa.starter.ig.IImplementationGuideOperationProvider;
|
import ca.uhn.fhir.jpa.starter.ig.IImplementationGuideOperationProvider;
|
||||||
import ca.uhn.fhir.jpa.subscription.util.SubscriptionDebugLogInterceptor;
|
import ca.uhn.fhir.jpa.subscription.util.SubscriptionDebugLogInterceptor;
|
||||||
@@ -149,6 +149,7 @@ public class StarterJpaConfig {
|
|||||||
@Primary
|
@Primary
|
||||||
@Bean
|
@Bean
|
||||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
|
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
|
||||||
|
Optional<ElasticsearchBootSvcImpl> elasticsearchSvc,
|
||||||
JpaProperties theJpaProperties,
|
JpaProperties theJpaProperties,
|
||||||
DataSource myDataSource,
|
DataSource myDataSource,
|
||||||
ConfigurableListableBeanFactory myConfigurableListableBeanFactory,
|
ConfigurableListableBeanFactory myConfigurableListableBeanFactory,
|
||||||
@@ -321,7 +322,6 @@ public class StarterJpaConfig {
|
|||||||
Optional<CorsInterceptor> corsInterceptor,
|
Optional<CorsInterceptor> corsInterceptor,
|
||||||
IInterceptorBroadcaster interceptorBroadcaster,
|
IInterceptorBroadcaster interceptorBroadcaster,
|
||||||
Optional<BinaryAccessProvider> binaryAccessProvider,
|
Optional<BinaryAccessProvider> binaryAccessProvider,
|
||||||
BinaryStorageInterceptor binaryStorageInterceptor,
|
|
||||||
IValidatorModule validatorModule,
|
IValidatorModule validatorModule,
|
||||||
Optional<GraphQLProvider> graphQLProvider,
|
Optional<GraphQLProvider> graphQLProvider,
|
||||||
BulkDataExportProvider bulkDataExportProvider,
|
BulkDataExportProvider bulkDataExportProvider,
|
||||||
@@ -453,7 +453,6 @@ public class StarterJpaConfig {
|
|||||||
// Binary Storage
|
// Binary Storage
|
||||||
if (appProperties.getBinary_storage_enabled() && binaryAccessProvider.isPresent()) {
|
if (appProperties.getBinary_storage_enabled() && binaryAccessProvider.isPresent()) {
|
||||||
fhirServer.registerProvider(binaryAccessProvider.get());
|
fhirServer.registerProvider(binaryAccessProvider.get());
|
||||||
fhirServer.registerInterceptor(binaryStorageInterceptor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc {
|
|||||||
|
|
||||||
private static final String OBSERVATION_RESOURCE_NAME = "Observation";
|
private static final String OBSERVATION_RESOURCE_NAME = "Observation";
|
||||||
|
|
||||||
private final ElasticsearchClient myRestHighLevelClient;
|
private final ElasticsearchClient myElasticsearchClient;
|
||||||
|
|
||||||
private final FhirContext myContext;
|
private final FhirContext myContext;
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc {
|
|||||||
public ElasticsearchBootSvcImpl(ElasticsearchClient client, FhirContext fhirContext, AppProperties appProperties) {
|
public ElasticsearchBootSvcImpl(ElasticsearchClient client, FhirContext fhirContext, AppProperties appProperties) {
|
||||||
|
|
||||||
myContext = fhirContext;
|
myContext = fhirContext;
|
||||||
myRestHighLevelClient = client;
|
myElasticsearchClient = client;
|
||||||
|
|
||||||
// Determine index prefix from configuration
|
// Determine index prefix from configuration
|
||||||
if (appProperties.getElasticsearch() != null) {
|
if (appProperties.getElasticsearch() != null) {
|
||||||
@@ -144,7 +144,7 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean createIndex(String theIndexName, String theMapping) throws IOException {
|
private boolean createIndex(String theIndexName, String theMapping) throws IOException {
|
||||||
return myRestHighLevelClient
|
return myElasticsearchClient
|
||||||
.indices()
|
.indices()
|
||||||
.create(cir -> cir.index(theIndexName).withJson(new StringReader(theMapping)))
|
.create(cir -> cir.index(theIndexName).withJson(new StringReader(theMapping)))
|
||||||
.acknowledged();
|
.acknowledged();
|
||||||
@@ -152,7 +152,7 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc {
|
|||||||
|
|
||||||
private boolean indexExists(String theIndexName) throws IOException {
|
private boolean indexExists(String theIndexName) throws IOException {
|
||||||
ExistsRequest request = new ExistsRequest.Builder().index(theIndexName).build();
|
ExistsRequest request = new ExistsRequest.Builder().index(theIndexName).build();
|
||||||
return myRestHighLevelClient.indices().exists(request).value();
|
return myElasticsearchClient.indices().exists(request).value();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -165,7 +165,7 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc {
|
|||||||
SearchRequest searchRequest = buildObservationResourceSearchRequest(thePids);
|
SearchRequest searchRequest = buildObservationResourceSearchRequest(thePids);
|
||||||
try {
|
try {
|
||||||
SearchResponse<ObservationJson> observationDocumentResponse =
|
SearchResponse<ObservationJson> observationDocumentResponse =
|
||||||
myRestHighLevelClient.search(searchRequest, ObservationJson.class);
|
myElasticsearchClient.search(searchRequest, ObservationJson.class);
|
||||||
List<Hit<ObservationJson>> observationDocumentHits =
|
List<Hit<ObservationJson>> observationDocumentHits =
|
||||||
observationDocumentResponse.hits().hits();
|
observationDocumentResponse.hits().hits();
|
||||||
IParser parser = TolerantJsonParser.createWithLenientErrorHandling(myContext, null);
|
IParser parser = TolerantJsonParser.createWithLenientErrorHandling(myContext, null);
|
||||||
@@ -202,7 +202,7 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc {
|
|||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public void refreshIndex(String theIndexName) throws IOException {
|
public void refreshIndex(String theIndexName) throws IOException {
|
||||||
myRestHighLevelClient.indices().refresh(fn -> fn.index(theIndexName));
|
myElasticsearchClient.indices().refresh(fn -> fn.index(theIndexName));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package ca.uhn.fhir.jpa.starter.validation;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration that enables versioned URL fallback behavior for FHIR validation.
|
||||||
|
*
|
||||||
|
* This wraps the validation support chain to add fallback logic for versioned canonical URLs.
|
||||||
|
* When a versioned URL like "http://hl7.org/fhir/StructureDefinition/Organization|4.0.1"
|
||||||
|
* cannot be found, it will automatically fall back to Non-versioned URL (without the |version suffix)
|
||||||
|
*
|
||||||
|
* This is useful when Implementation Guides reference versioned base FHIR resources
|
||||||
|
* that aren't loaded with exact version matching.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class VersionedUrlFallbackConfig {
|
||||||
|
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(VersionedUrlFallbackConfig.class);
|
||||||
|
|
||||||
|
public VersionedUrlFallbackConfig(FhirContext theFhirContext, ValidationSupportChain theValidationSupportChain) {
|
||||||
|
ourLog.info("Adding VersionedUrlFallbackValidationSupport to validation chain");
|
||||||
|
theValidationSupportChain.addValidationSupport(
|
||||||
|
0, new VersionedUrlFallbackValidationSupport(theFhirContext, theValidationSupportChain));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package ca.uhn.fhir.jpa.starter.validation;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A validation support that provides fallback behavior for versioned canonical URLs.
|
||||||
|
*
|
||||||
|
* When a versioned URL like "http://hl7.org/fhir/StructureDefinition/Organization|4.0.1"
|
||||||
|
* is requested, this support first tries the exact versioned URL, then falls back to
|
||||||
|
* the non-versioned URL if not found.
|
||||||
|
*
|
||||||
|
* For non-versioned URLs or URLs not matching the configured prefixes, this support
|
||||||
|
* returns null to let other supports in the chain handle the request.
|
||||||
|
*
|
||||||
|
* This addresses issues where profiles reference versioned base FHIR resources that
|
||||||
|
* aren't available with exact version matching in the validation context.
|
||||||
|
*/
|
||||||
|
// TODO: this should be fixed in core
|
||||||
|
public class VersionedUrlFallbackValidationSupport implements IValidationSupport {
|
||||||
|
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(VersionedUrlFallbackValidationSupport.class);
|
||||||
|
|
||||||
|
private final FhirContext myFhirContext;
|
||||||
|
private final IValidationSupport myChain;
|
||||||
|
private final Set<String> myUrlPrefixes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a fallback validation support that only applies to URLs starting with the default prefix
|
||||||
|
* (http://hl7.org/fhir/StructureDefinition/).
|
||||||
|
*/
|
||||||
|
public VersionedUrlFallbackValidationSupport(FhirContext theFhirContext, IValidationSupport theChain) {
|
||||||
|
this(theFhirContext, theChain, Set.of(URL_PREFIX_STRUCTURE_DEFINITION));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a fallback validation support that only applies to URLs starting with the specified prefixes.
|
||||||
|
*
|
||||||
|
* @param theFhirContext the FHIR context
|
||||||
|
* @param theChain the validation support chain to delegate fallback lookups to
|
||||||
|
* @param theUrlPrefixes the URL prefixes to apply fallback logic to (e.g., "http://hl7.org/fhir/StructureDefinition/").
|
||||||
|
* Pass an empty set to apply to all URLs.
|
||||||
|
*/
|
||||||
|
public VersionedUrlFallbackValidationSupport(
|
||||||
|
FhirContext theFhirContext, IValidationSupport theChain, Set<String> theUrlPrefixes) {
|
||||||
|
myFhirContext = theFhirContext;
|
||||||
|
myChain = theChain;
|
||||||
|
myUrlPrefixes = theUrlPrefixes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FhirContext getFhirContext() {
|
||||||
|
return myFhirContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends IBaseResource> T fetchResource(Class<T> theClass, String theUri) {
|
||||||
|
return doFetchWithFallback(theUri, uri -> myChain.fetchResource(theClass, uri));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBaseResource fetchStructureDefinition(String theUrl) {
|
||||||
|
return doFetchWithFallback(theUrl, myChain::fetchStructureDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends IBaseResource> T doFetchWithFallback(String theUrl, Function<String, T> theFetcher) {
|
||||||
|
// Check if this is a versioned URL (contains |)
|
||||||
|
int pipeIndex = theUrl.indexOf('|');
|
||||||
|
if (pipeIndex <= 0) {
|
||||||
|
// Not a versioned URL, let other supports handle it
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String baseUrl = theUrl.substring(0, pipeIndex);
|
||||||
|
|
||||||
|
// Check if this URL matches our configured prefixes
|
||||||
|
if (!matchesPrefix(baseUrl)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try exact versioned URL first
|
||||||
|
T result = theFetcher.apply(theUrl);
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try non-versioned URL fallback
|
||||||
|
result = theFetcher.apply(baseUrl);
|
||||||
|
if (result != null) {
|
||||||
|
ourLog.warn(
|
||||||
|
"Requested versioned canonical '{}' not found, falling back to non-versioned '{}'",
|
||||||
|
theUrl,
|
||||||
|
baseUrl);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesPrefix(String theUrl) {
|
||||||
|
if (myUrlPrefixes.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (String prefix : myUrlPrefixes) {
|
||||||
|
if (theUrl.startsWith(prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "VersionedUrlFallbackValidationSupport";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -267,6 +267,7 @@ hapi:
|
|||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
bulk_export_enabled: false
|
bulk_export_enabled: false
|
||||||
bulk_import_enabled: false
|
bulk_import_enabled: false
|
||||||
|
bulk_export_file_retention_period_hours: 2
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
# F. Write / Delete / Integrity
|
# F. Write / Delete / Integrity
|
||||||
|
|||||||
50
src/main/resources/application-elastic.yaml
Normal file
50
src/main/resources/application-elastic.yaml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
spring:
|
||||||
|
elasticsearch:
|
||||||
|
uris: http://localhost:9200
|
||||||
|
username: elastic
|
||||||
|
password: elastic
|
||||||
|
|
||||||
|
autoconfigure:
|
||||||
|
# This empty exclude is needed to override the default exclusion of the Elasticsearch configuration.
|
||||||
|
exclude:
|
||||||
|
|
||||||
|
jpa:
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
# --- Hibernate Search (Lucene/Elasticsearch) ---
|
||||||
|
# Note: the following values should be kept in sync with ca.uhn.fhir.jpa.search.elastic.ElasticsearchHibernatePropertiesBuilder
|
||||||
|
search:
|
||||||
|
schema_management:
|
||||||
|
strategy: CREATE
|
||||||
|
enabled: true
|
||||||
|
backend:
|
||||||
|
layout:
|
||||||
|
strategy: ca.uhn.fhir.jpa.search.elastic.IndexNamePrefixLayoutStrategy
|
||||||
|
type: elasticsearch
|
||||||
|
protocol: http
|
||||||
|
analysis:
|
||||||
|
configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticsearchAnalysisConfigurer
|
||||||
|
scroll_timeout: 60
|
||||||
|
schema_management:
|
||||||
|
settings_file: ca/uhn/fhir/jpa/elastic/index-settings.json
|
||||||
|
minimal_required_status_wait_timeout: 10000
|
||||||
|
minimal_required_status: YELLOW
|
||||||
|
|
||||||
|
dynamic_mapping: true
|
||||||
|
indexing:
|
||||||
|
plan:
|
||||||
|
synchronization:
|
||||||
|
strategy: async
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------
|
||||||
|
# HAPI FHIR — grouped by domain
|
||||||
|
# -------------------------------------------------------------------------------------
|
||||||
|
hapi:
|
||||||
|
fhir:
|
||||||
|
# -------------------------------------------------------------------------------
|
||||||
|
# D. Search & Indexing
|
||||||
|
# -------------------------------------------------------------------------------
|
||||||
|
# NOTE: Extended Lucene/Elasticsearch indexing is experimental.
|
||||||
|
# See https://hapifhir.io/hapi-fhir/docs/server_jpa/elastic.html
|
||||||
|
advanced_lucene_indexing: true
|
||||||
|
search_index_full_text_enabled: true
|
||||||
@@ -37,6 +37,10 @@ management:
|
|||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
|
# elasticsearch:
|
||||||
|
# uris: http://localhost:9200
|
||||||
|
# username: elastic
|
||||||
|
# password: elastic
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
# Application Name
|
# Application Name
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
@@ -130,8 +134,10 @@ spring:
|
|||||||
use_minimal_puts: false
|
use_minimal_puts: false
|
||||||
|
|
||||||
# --- Hibernate Search (Lucene/Elasticsearch) ---
|
# --- Hibernate Search (Lucene/Elasticsearch) ---
|
||||||
search:
|
#search:
|
||||||
enabled: false
|
# schema_management:
|
||||||
|
# strategy: CREATE
|
||||||
|
# enabled: true
|
||||||
# Lucene backend (default example)
|
# Lucene backend (default example)
|
||||||
# backend:
|
# backend:
|
||||||
# type: lucene
|
# type: lucene
|
||||||
@@ -142,10 +148,25 @@ spring:
|
|||||||
# root: target/lucenefiles
|
# root: target/lucenefiles
|
||||||
# lucene_version: lucene_current
|
# lucene_version: lucene_current
|
||||||
# Elasticsearch backend (alternative) — see also hapi.fhir.elasticsearch section in docs
|
# Elasticsearch backend (alternative) — see also hapi.fhir.elasticsearch section in docs
|
||||||
# backend:
|
# backend:
|
||||||
# type: elasticsearch
|
# layout:
|
||||||
# analysis:
|
# strategy: ca.uhn.fhir.jpa.search.elastic.IndexNamePrefixLayoutStrategy
|
||||||
# configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer
|
# type: elasticsearch
|
||||||
|
# protocol: http
|
||||||
|
# analysis:
|
||||||
|
# configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticsearchAnalysisConfigurer
|
||||||
|
# scroll_timeout: 60
|
||||||
|
# schema_management:
|
||||||
|
# settings_file: ca/uhn/fhir/jpa/elastic/index-settings.json
|
||||||
|
# minimal_required_status_wait_timeout: 10000
|
||||||
|
# minimal_required_status: YELLOW
|
||||||
|
#
|
||||||
|
# dynamic_mapping: true
|
||||||
|
# indexing:
|
||||||
|
# plan:
|
||||||
|
# synchronization:
|
||||||
|
# strategy: async
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------
|
||||||
# HAPI FHIR — grouped by domain
|
# HAPI FHIR — grouped by domain
|
||||||
@@ -249,8 +270,8 @@ hapi:
|
|||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
# NOTE: Extended Lucene/Elasticsearch indexing is experimental.
|
# NOTE: Extended Lucene/Elasticsearch indexing is experimental.
|
||||||
# See https://hapifhir.io/hapi-fhir/docs/server_jpa/elastic.html
|
# See https://hapifhir.io/hapi-fhir/docs/server_jpa/elastic.html
|
||||||
advanced_lucene_indexing: false
|
# advanced_lucene_indexing: true
|
||||||
search_index_full_text_enabled: false
|
# search_index_full_text_enabled: true
|
||||||
# language_search_parameter_enabled: true
|
# language_search_parameter_enabled: true
|
||||||
# upliftedRefchains_enabled: true
|
# upliftedRefchains_enabled: true
|
||||||
# index_storage_optimized: false
|
# index_storage_optimized: false
|
||||||
@@ -270,6 +291,7 @@ hapi:
|
|||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
bulk_export_enabled: false
|
bulk_export_enabled: false
|
||||||
bulk_import_enabled: false
|
bulk_import_enabled: false
|
||||||
|
bulk_export_file_retention_period_hours: 2
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
# F. Write / Delete / Integrity
|
# F. Write / Delete / Integrity
|
||||||
@@ -408,7 +430,7 @@ hapi:
|
|||||||
# binary_storage_filesystem_base_directory: /binstore
|
# binary_storage_filesystem_base_directory: /binstore
|
||||||
# When binary_storage_mode is FILESYSTEM and this value is not set,
|
# When binary_storage_mode is FILESYSTEM and this value is not set,
|
||||||
# the starter defaults to 102400 bytes so smaller binaries stay inline.
|
# the starter defaults to 102400 bytes so smaller binaries stay inline.
|
||||||
inline_resource_storage_below_size: 4000
|
binary_storage_minimum_binary_size: 4000
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
# P. Remote Terminology Service (disabled by default)
|
# P. Remote Terminology Service (disabled by default)
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ class BinaryStorageFilesystemDefaultIT extends BaseBinaryStorageIntegrationTest
|
|||||||
"hapi.fhir.binary_storage_enabled=true",
|
"hapi.fhir.binary_storage_enabled=true",
|
||||||
"hapi.fhir.binary_storage_mode=FILESYSTEM",
|
"hapi.fhir.binary_storage_mode=FILESYSTEM",
|
||||||
"hapi.fhir.binary_storage_filesystem_base_directory=target/test-binary-storage/filesystem-custom",
|
"hapi.fhir.binary_storage_filesystem_base_directory=target/test-binary-storage/filesystem-custom",
|
||||||
"hapi.fhir.inline_resource_storage_below_size=32768"
|
"hapi.fhir.binary_storage_minimum_binary_size=32768"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
class BinaryStorageFilesystemCustomThresholdIT extends BaseBinaryStorageIntegrationTest {
|
class BinaryStorageFilesystemCustomThresholdIT extends BaseBinaryStorageIntegrationTest {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package ca.uhn.fhir.jpa.starter;
|
package ca.uhn.fhir.jpa.starter;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl;
|
import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl;
|
||||||
import ca.uhn.fhir.jpa.starter.common.TestContainerHelper;
|
import ca.uhn.fhir.jpa.starter.common.TestContainerHelper;
|
||||||
@@ -36,69 +34,72 @@ import org.testcontainers.elasticsearch.ElasticsearchContainer;
|
|||||||
import org.testcontainers.junit.jupiter.Container;
|
import org.testcontainers.junit.jupiter.Container;
|
||||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
@Testcontainers
|
@Testcontainers
|
||||||
@ActiveProfiles("test")
|
@ActiveProfiles("test")
|
||||||
@TestPropertySource(locations = "classpath:test-elasticsearch-lastn.yaml")
|
@TestPropertySource(locations = "classpath:test-elasticsearch-lastn.yaml")
|
||||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class})
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class})
|
||||||
class ElasticsearchLastNR4IT {
|
class ElasticsearchLastNR4IT {
|
||||||
private IGenericClient ourClient;
|
private IGenericClient ourClient;
|
||||||
|
|
||||||
@Container
|
@Container
|
||||||
private static final ElasticsearchContainer ELASTICSEARCH = TestContainerHelper.newElasticsearchContainer()
|
private static final ElasticsearchContainer ELASTICSEARCH = TestContainerHelper.newElasticsearchContainer()
|
||||||
// Set index defaults to handle HAPI FHIR's MAX_SUBSCRIPTION_RESULTS (50000)
|
// Set index defaults to handle HAPI FHIR's MAX_SUBSCRIPTION_RESULTS (50000)
|
||||||
.withEnv("indices.query.bool.max_clause_count", "50000");
|
.withEnv("indices.query.bool.max_clause_count", "50000");
|
||||||
|
|
||||||
@DynamicPropertySource
|
@DynamicPropertySource
|
||||||
static void registerElasticsearchProperties(DynamicPropertyRegistry registry) {
|
static void registerElasticsearchProperties(DynamicPropertyRegistry registry) {
|
||||||
TestContainerHelper.registerElasticsearchProperties(registry, ELASTICSEARCH);
|
TestContainerHelper.registerElasticsearchProperties(registry, ELASTICSEARCH);
|
||||||
// Also register spring.elasticsearch.uris for ElasticConfigCondition to enable ElasticsearchBootSvcImpl
|
// Also register spring.elasticsearch.uris for ElasticConfigCondition to enable ElasticsearchBootSvcImpl
|
||||||
registry.add("spring.elasticsearch.uris", () -> TestContainerHelper.getElasticsearchHttpUrl(ELASTICSEARCH));
|
registry.add("spring.elasticsearch.uris", () -> TestContainerHelper.getElasticsearchHttpUrl(ELASTICSEARCH));
|
||||||
}
|
|
||||||
|
|
||||||
@Autowired
|
registry.add("spring.jpa.properties.hibernate.search.backend.hosts", ELASTICSEARCH::getHttpHostAddress);
|
||||||
private ElasticsearchBootSvcImpl myElasticsearchSvc;
|
registry.add("spring.jpa.properties.hibernate.search.backend.protocol", () -> "http");
|
||||||
|
registry.add("spring.jpa.properties.hibernate.search.backend.username", () -> "");
|
||||||
|
registry.add("spring.jpa.properties.hibernate.search.backend.password", () -> "");
|
||||||
|
}
|
||||||
|
|
||||||
@LocalServerPort
|
@Autowired
|
||||||
private int port;
|
private ElasticsearchBootSvcImpl myElasticsearchSvc;
|
||||||
|
|
||||||
@Test
|
@LocalServerPort
|
||||||
void testLastN() throws IOException, InterruptedException {
|
private int port;
|
||||||
Thread.sleep(2000);
|
|
||||||
|
|
||||||
Patient pt = new Patient();
|
@Test
|
||||||
pt.addName().setFamily("Lastn").addGiven("Arthur");
|
void testLastN() throws IOException, InterruptedException {
|
||||||
IIdType id = ourClient.create().resource(pt).execute().getId().toUnqualifiedVersionless();
|
Patient pt = new Patient();
|
||||||
|
pt.addName().setFamily("Lastn").addGiven("Arthur");
|
||||||
|
IIdType id = ourClient.create().resource(pt).execute().getId().toUnqualifiedVersionless();
|
||||||
|
|
||||||
Observation obs = new Observation();
|
Observation obs = new Observation();
|
||||||
obs.getSubject().setReferenceElement(id);
|
obs.getSubject().setReferenceElement(id);
|
||||||
String observationCode = "testobservationcode";
|
String observationCode = "testobservationcode";
|
||||||
|
|
||||||
obs.getCode().addCoding().setCode(observationCode).setSystem("http://testobservationcodesystem");
|
|
||||||
obs.setValue(new StringType(observationCode));
|
|
||||||
|
|
||||||
Date effectiveDtm = new GregorianCalendar().getTime();
|
obs.getCode().addCoding().setCode(observationCode).setSystem("http://testobservationcodesystem");
|
||||||
obs.setEffective(new DateTimeType(effectiveDtm));
|
obs.setValue(new StringType(observationCode));
|
||||||
obs.getCategoryFirstRep().addCoding().setCode("testcategorycode").setSystem("http://testcategorycodesystem");
|
|
||||||
IIdType obsId = ourClient.create().resource(obs).execute().getId().toUnqualifiedVersionless();
|
|
||||||
|
|
||||||
myElasticsearchSvc.refreshIndex(ElasticsearchSvcImpl.OBSERVATION_INDEX);
|
Date effectiveDtm = new GregorianCalendar().getTime();
|
||||||
Thread.sleep(2000);
|
obs.setEffective(new DateTimeType(effectiveDtm));
|
||||||
|
obs.getCategoryFirstRep().addCoding().setCode("testcategorycode").setSystem("http://testcategorycodesystem");
|
||||||
|
IIdType obsId = ourClient.create().resource(obs).execute().getId().toUnqualifiedVersionless();
|
||||||
|
|
||||||
Parameters output = ourClient.operation().onType(Observation.class).named("lastn")
|
myElasticsearchSvc.refreshIndex(ElasticsearchSvcImpl.OBSERVATION_INDEX);
|
||||||
.withParameter(Parameters.class, "max", new IntegerType(1))
|
|
||||||
.andParameter("subject", new StringType("Patient/" + id.getIdPart()))
|
|
||||||
.execute();
|
|
||||||
Bundle b = (Bundle) output.getParameter().get(0).getResource();
|
|
||||||
assertEquals(1, b.getTotal());
|
|
||||||
assertEquals(obsId, b.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless());
|
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeEach
|
Thread.sleep(2000);
|
||||||
void beforeEach() {
|
Parameters output = ourClient.operation().onType(Observation.class).named("lastn").withParameter(Parameters.class, "max", new IntegerType(1)).andParameter("subject", new StringType("Patient/" + id.getIdPart())).execute();
|
||||||
FhirContext ctx = FhirContext.forR4();
|
Bundle b = (Bundle) output.getParameter().get(0).getResource();
|
||||||
ctx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
|
assertEquals(1, b.getTotal());
|
||||||
ctx.getRestfulClientFactory().setSocketTimeout((int) Duration.ofMinutes(20).toMillis());
|
assertEquals(obsId, b.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless());
|
||||||
ourClient = ctx.newRestfulGenericClient("http://localhost:" + port + "/fhir/");
|
}
|
||||||
ourClient.registerInterceptor(new LoggingInterceptor(true));
|
|
||||||
}
|
@BeforeEach
|
||||||
|
void beforeEach() {
|
||||||
|
FhirContext ctx = FhirContext.forR4();
|
||||||
|
ctx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
|
||||||
|
ctx.getRestfulClientFactory().setSocketTimeout((int) Duration.ofMinutes(20).toMillis());
|
||||||
|
ourClient = ctx.newRestfulGenericClient("http://localhost:" + port + "/fhir/");
|
||||||
|
ourClient.registerInterceptor(new LoggingInterceptor(true));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class FhirServerConfigCommonBinaryStorageTest {
|
|||||||
void filesystemModeHonoursExplicitMinimum() throws Exception {
|
void filesystemModeHonoursExplicitMinimum() throws Exception {
|
||||||
AppProperties props = new AppProperties();
|
AppProperties props = new AppProperties();
|
||||||
props.setBinary_storage_mode(AppProperties.BinaryStorageMode.FILESYSTEM);
|
props.setBinary_storage_mode(AppProperties.BinaryStorageMode.FILESYSTEM);
|
||||||
props.setInline_resource_storage_below_size(4096);
|
props.setBinary_storage_minimum_binary_size(4096);
|
||||||
Path baseDir = tempDir.resolve("fs-min-explicit");
|
Path baseDir = tempDir.resolve("fs-min-explicit");
|
||||||
Files.createDirectories(baseDir);
|
Files.createDirectories(baseDir);
|
||||||
props.setBinary_storage_filesystem_base_directory(baseDir.toString());
|
props.setBinary_storage_filesystem_base_directory(baseDir.toString());
|
||||||
@@ -62,7 +62,7 @@ class FhirServerConfigCommonBinaryStorageTest {
|
|||||||
void filesystemModeSupportsZeroMinimumWhenExplicit() throws Exception {
|
void filesystemModeSupportsZeroMinimumWhenExplicit() throws Exception {
|
||||||
AppProperties props = new AppProperties();
|
AppProperties props = new AppProperties();
|
||||||
props.setBinary_storage_mode(AppProperties.BinaryStorageMode.FILESYSTEM);
|
props.setBinary_storage_mode(AppProperties.BinaryStorageMode.FILESYSTEM);
|
||||||
props.setInline_resource_storage_below_size(0);
|
props.setBinary_storage_minimum_binary_size(0);
|
||||||
Path baseDir = tempDir.resolve("fs-zero");
|
Path baseDir = tempDir.resolve("fs-zero");
|
||||||
Files.createDirectories(baseDir);
|
Files.createDirectories(baseDir);
|
||||||
props.setBinary_storage_filesystem_base_directory(baseDir.toString());
|
props.setBinary_storage_filesystem_base_directory(baseDir.toString());
|
||||||
|
|||||||
@@ -0,0 +1,447 @@
|
|||||||
|
package ca.uhn.fhir.jpa.starter.validation;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
|
||||||
|
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||||
|
import ca.uhn.fhir.validation.FhirValidator;
|
||||||
|
import ca.uhn.fhir.validation.ValidationResult;
|
||||||
|
import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService;
|
||||||
|
import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport;
|
||||||
|
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
|
||||||
|
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
|
||||||
|
import org.hl7.fhir.r4.model.Observation;
|
||||||
|
import org.hl7.fhir.r4.model.StructureDefinition;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static ca.uhn.fhir.context.support.IValidationSupport.URL_PREFIX_STRUCTURE_DEFINITION;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class VersionedUrlFallbackValidationSupportTest {
|
||||||
|
|
||||||
|
private static final String BASE_FHIR_SD_PREFIX = "http://hl7.org/fhir/StructureDefinition/";
|
||||||
|
private static final String ORGANIZATION_URL = BASE_FHIR_SD_PREFIX + "Organization";
|
||||||
|
private static final String ORGANIZATION_URL_VERSIONED = ORGANIZATION_URL + "|4.0.1";
|
||||||
|
|
||||||
|
private static final String CUSTOM_SD_URL = "http://example.com/StructureDefinition/MyProfile";
|
||||||
|
private static final String CUSTOM_SD_URL_VERSIONED = CUSTOM_SD_URL + "|1.0.0";
|
||||||
|
|
||||||
|
private FhirContext myFhirContext;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IValidationSupport myChain;
|
||||||
|
|
||||||
|
private VersionedUrlFallbackValidationSupport mySvc;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
myFhirContext = FhirContext.forR4Cached();
|
||||||
|
mySvc = new VersionedUrlFallbackValidationSupport(myFhirContext, myChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExactVersionedUrl_ReturnedWithoutFallback() {
|
||||||
|
// Setup: exact versioned URL is available
|
||||||
|
StructureDefinition sd = new StructureDefinition();
|
||||||
|
sd.setUrl(ORGANIZATION_URL);
|
||||||
|
sd.setVersion("4.0.1");
|
||||||
|
|
||||||
|
when(myChain.fetchStructureDefinition(ORGANIZATION_URL_VERSIONED)).thenReturn(sd);
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
var result = mySvc.fetchStructureDefinition(ORGANIZATION_URL_VERSIONED);
|
||||||
|
|
||||||
|
// Verify: returns exact match, no fallback attempted
|
||||||
|
assertNotNull(result);
|
||||||
|
assertSame(sd, result);
|
||||||
|
verify(myChain).fetchStructureDefinition(ORGANIZATION_URL_VERSIONED);
|
||||||
|
verify(myChain, never()).fetchStructureDefinition(ORGANIZATION_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFallbackToNonVersionedUrl() {
|
||||||
|
// Setup: exact versioned URL not found, non-versioned returns a resource
|
||||||
|
StructureDefinition sd = new StructureDefinition();
|
||||||
|
sd.setUrl(ORGANIZATION_URL);
|
||||||
|
|
||||||
|
when(myChain.fetchStructureDefinition(ORGANIZATION_URL_VERSIONED)).thenReturn(null);
|
||||||
|
when(myChain.fetchStructureDefinition(ORGANIZATION_URL)).thenReturn(sd);
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
var result = mySvc.fetchStructureDefinition(ORGANIZATION_URL_VERSIONED);
|
||||||
|
|
||||||
|
// Verify: fallback to non-versioned succeeds
|
||||||
|
assertNotNull(result);
|
||||||
|
assertSame(sd, result);
|
||||||
|
verify(myChain).fetchStructureDefinition(ORGANIZATION_URL_VERSIONED);
|
||||||
|
verify(myChain).fetchStructureDefinition(ORGANIZATION_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNoFallback_ForNonVersionedUrl() {
|
||||||
|
// Execute: non-versioned URL should pass through without any chain calls
|
||||||
|
var result = mySvc.fetchStructureDefinition(ORGANIZATION_URL);
|
||||||
|
|
||||||
|
// Verify: returns null immediately, lets other chain supports handle it
|
||||||
|
assertNull(result);
|
||||||
|
verifyNoInteractions(myChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNoFallback_ForCustomUrlNotMatchingDefaultPrefix() {
|
||||||
|
// Execute: custom URL doesn't match the default prefix filter
|
||||||
|
var result = mySvc.fetchStructureDefinition(CUSTOM_SD_URL_VERSIONED);
|
||||||
|
|
||||||
|
// Verify: returns null, doesn't attempt fallback (not in prefix list)
|
||||||
|
assertNull(result);
|
||||||
|
verifyNoInteractions(myChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFallback_ForCustomUrl_WhenPrefixConfigured() {
|
||||||
|
// Setup: configure to also handle custom URLs
|
||||||
|
mySvc = new VersionedUrlFallbackValidationSupport(myFhirContext, myChain,
|
||||||
|
Set.of(BASE_FHIR_SD_PREFIX, "http://example.com/StructureDefinition/"));
|
||||||
|
|
||||||
|
StructureDefinition sd = new StructureDefinition();
|
||||||
|
sd.setUrl(CUSTOM_SD_URL);
|
||||||
|
|
||||||
|
when(myChain.fetchStructureDefinition(CUSTOM_SD_URL_VERSIONED)).thenReturn(null);
|
||||||
|
when(myChain.fetchStructureDefinition(CUSTOM_SD_URL)).thenReturn(sd);
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
var result = mySvc.fetchStructureDefinition(CUSTOM_SD_URL_VERSIONED);
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertNotNull(result);
|
||||||
|
assertSame(sd, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFallback_ForAllUrls_WhenEmptyPrefixSet() {
|
||||||
|
// Setup: empty prefix set means apply to all URLs
|
||||||
|
mySvc = new VersionedUrlFallbackValidationSupport(myFhirContext, myChain, Set.of());
|
||||||
|
|
||||||
|
StructureDefinition sd = new StructureDefinition();
|
||||||
|
sd.setUrl(CUSTOM_SD_URL);
|
||||||
|
|
||||||
|
when(myChain.fetchStructureDefinition(CUSTOM_SD_URL_VERSIONED)).thenReturn(null);
|
||||||
|
when(myChain.fetchStructureDefinition(CUSTOM_SD_URL)).thenReturn(sd);
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
var result = mySvc.fetchStructureDefinition(CUSTOM_SD_URL_VERSIONED);
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertNotNull(result);
|
||||||
|
assertSame(sd, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFetchResource_FallbackToNonVersioned() {
|
||||||
|
// Setup
|
||||||
|
StructureDefinition sd = new StructureDefinition();
|
||||||
|
sd.setUrl(ORGANIZATION_URL);
|
||||||
|
|
||||||
|
when(myChain.fetchResource(StructureDefinition.class, ORGANIZATION_URL_VERSIONED)).thenReturn(null);
|
||||||
|
when(myChain.fetchResource(StructureDefinition.class, ORGANIZATION_URL)).thenReturn(sd);
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
var result = mySvc.fetchResource(StructureDefinition.class, ORGANIZATION_URL_VERSIONED);
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertNotNull(result);
|
||||||
|
assertSame(sd, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFetchResource_NoFallbackForNonMatchingPrefix() {
|
||||||
|
// Execute
|
||||||
|
var result = mySvc.fetchResource(StructureDefinition.class, CUSTOM_SD_URL_VERSIONED);
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertNull(result);
|
||||||
|
verifyNoInteractions(myChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFetchResource_NoFallbackForNonVersionedUrl() {
|
||||||
|
// Execute
|
||||||
|
var result = mySvc.fetchResource(StructureDefinition.class, ORGANIZATION_URL);
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertNull(result);
|
||||||
|
verifyNoInteractions(myChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testReturnsNull_WhenNoFallbackSucceeds() {
|
||||||
|
// Setup: nothing found in any lookup
|
||||||
|
when(myChain.fetchStructureDefinition(ORGANIZATION_URL_VERSIONED)).thenReturn(null);
|
||||||
|
when(myChain.fetchStructureDefinition(ORGANIZATION_URL)).thenReturn(null);
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
var result = mySvc.fetchStructureDefinition(ORGANIZATION_URL_VERSIONED);
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertNull(result);
|
||||||
|
verify(myChain).fetchStructureDefinition(ORGANIZATION_URL_VERSIONED);
|
||||||
|
verify(myChain).fetchStructureDefinition(ORGANIZATION_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetName() {
|
||||||
|
assertEquals("VersionedUrlFallbackValidationSupport", mySvc.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetFhirContext() {
|
||||||
|
assertSame(myFhirContext, mySvc.getFhirContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDefaultUrlPrefix() {
|
||||||
|
assertEquals("http://hl7.org/fhir/StructureDefinition/",
|
||||||
|
URL_PREFIX_STRUCTURE_DEFINITION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests using real DefaultProfileValidationSupport instead of mocks.
|
||||||
|
* This tests the actual fallback behavior with FHIR's built-in profiles.
|
||||||
|
*/
|
||||||
|
@Nested
|
||||||
|
class WithRealValidationChain {
|
||||||
|
|
||||||
|
private FhirContext myFhirContext;
|
||||||
|
private ValidationSupportChain myValidationChain;
|
||||||
|
private VersionedUrlFallbackValidationSupport mySvc;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
myFhirContext = FhirContext.forR4Cached();
|
||||||
|
|
||||||
|
// Create a validation chain with the real DefaultProfileValidationSupport
|
||||||
|
// which contains all built-in FHIR R4 StructureDefinitions
|
||||||
|
myValidationChain = new ValidationSupportChain(new DefaultProfileValidationSupport(myFhirContext));
|
||||||
|
|
||||||
|
// Wrap the chain with our fallback support, similar to production setup
|
||||||
|
mySvc = new VersionedUrlFallbackValidationSupport(myFhirContext, myValidationChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFallbackToNonVersionedUrl_WithRealDefaultProfile() {
|
||||||
|
// The DefaultProfileValidationSupport has Organization without version suffix.
|
||||||
|
// When we request versioned URL, it should fall back and find it.
|
||||||
|
String versionedUrl = "http://hl7.org/fhir/StructureDefinition/Organization|4.0.1";
|
||||||
|
|
||||||
|
var result = mySvc.fetchStructureDefinition(versionedUrl);
|
||||||
|
|
||||||
|
assertNotNull(result, "Should find Organization via fallback to non-versioned URL");
|
||||||
|
assertInstanceOf(StructureDefinition.class, result);
|
||||||
|
StructureDefinition sd = (StructureDefinition) result;
|
||||||
|
assertEquals("http://hl7.org/fhir/StructureDefinition/Organization", sd.getUrl());
|
||||||
|
assertEquals("Organization", sd.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFallbackForPatient_WithRealDefaultProfile() {
|
||||||
|
String versionedUrl = "http://hl7.org/fhir/StructureDefinition/Patient|4.0.1";
|
||||||
|
|
||||||
|
var result = mySvc.fetchStructureDefinition(versionedUrl);
|
||||||
|
|
||||||
|
assertNotNull(result, "Should find Patient via fallback");
|
||||||
|
assertInstanceOf(StructureDefinition.class, result);
|
||||||
|
StructureDefinition sd = (StructureDefinition) result;
|
||||||
|
assertEquals("http://hl7.org/fhir/StructureDefinition/Patient", sd.getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFetchResource_WithRealDefaultProfile() {
|
||||||
|
String versionedUrl = "http://hl7.org/fhir/StructureDefinition/Observation|4.0.1";
|
||||||
|
|
||||||
|
var result = mySvc.fetchResource(StructureDefinition.class, versionedUrl);
|
||||||
|
|
||||||
|
assertNotNull(result, "Should find Observation via fetchResource fallback");
|
||||||
|
assertEquals("http://hl7.org/fhir/StructureDefinition/Observation", result.getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNonExistentResource_ReturnsNull() {
|
||||||
|
String versionedUrl = "http://hl7.org/fhir/StructureDefinition/NonExistentResource|1.0.0";
|
||||||
|
|
||||||
|
var result = mySvc.fetchStructureDefinition(versionedUrl);
|
||||||
|
|
||||||
|
assertNull(result, "Should return null for non-existent resource");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNonVersionedUrl_PassesThrough() {
|
||||||
|
// Non-versioned URLs should return null from the fallback support
|
||||||
|
// (they're handled by DefaultProfileValidationSupport directly in a real chain)
|
||||||
|
String nonVersionedUrl = "http://hl7.org/fhir/StructureDefinition/Patient";
|
||||||
|
|
||||||
|
var result = mySvc.fetchStructureDefinition(nonVersionedUrl);
|
||||||
|
|
||||||
|
// The fallback support returns null for non-versioned URLs
|
||||||
|
// In a real setup, the chain would handle this
|
||||||
|
assertNull(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDataTypeProfiles_WithRealDefaultProfile() {
|
||||||
|
// Test that data type StructureDefinitions also work
|
||||||
|
String versionedUrl = "http://hl7.org/fhir/StructureDefinition/HumanName|4.0.1";
|
||||||
|
|
||||||
|
var result = mySvc.fetchStructureDefinition(versionedUrl);
|
||||||
|
|
||||||
|
assertNotNull(result, "Should find HumanName data type via fallback");
|
||||||
|
assertInstanceOf(StructureDefinition.class, result);
|
||||||
|
assertEquals("http://hl7.org/fhir/StructureDefinition/HumanName",
|
||||||
|
((StructureDefinition) result).getUrl());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests where VersionedUrlFallbackValidationSupport is part of the
|
||||||
|
* ValidationSupportChain (as in production) rather than wrapping it.
|
||||||
|
*/
|
||||||
|
@Nested
|
||||||
|
class WithFallbackInChain {
|
||||||
|
|
||||||
|
private FhirContext myFhirContext;
|
||||||
|
private ValidationSupportChain myValidationChain;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
myFhirContext = FhirContext.forR4Cached();
|
||||||
|
DefaultProfileValidationSupport defaultSupport = new DefaultProfileValidationSupport(myFhirContext);
|
||||||
|
|
||||||
|
// Create a chain where VersionedUrlFallbackValidationSupport is a member
|
||||||
|
// This mimics production setup where the fallback is part of the chain
|
||||||
|
myValidationChain = new ValidationSupportChain(defaultSupport);
|
||||||
|
VersionedUrlFallbackValidationSupport fallbackSupport =
|
||||||
|
new VersionedUrlFallbackValidationSupport(myFhirContext, myValidationChain);
|
||||||
|
|
||||||
|
// Rebuild chain with fallback support first (higher priority)
|
||||||
|
myValidationChain = new ValidationSupportChain(fallbackSupport, defaultSupport);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testChainResolvesVersionedUrl() {
|
||||||
|
String versionedUrl = "http://hl7.org/fhir/StructureDefinition/Patient|4.0.1";
|
||||||
|
|
||||||
|
var result = myValidationChain.fetchStructureDefinition(versionedUrl);
|
||||||
|
|
||||||
|
assertNotNull(result, "Chain should resolve versioned URL via fallback");
|
||||||
|
assertInstanceOf(StructureDefinition.class, result);
|
||||||
|
assertEquals("http://hl7.org/fhir/StructureDefinition/Patient",
|
||||||
|
((StructureDefinition) result).getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testChainResolvesNonVersionedUrl() {
|
||||||
|
// Non-versioned URLs should still work (handled by DefaultProfileValidationSupport)
|
||||||
|
String nonVersionedUrl = "http://hl7.org/fhir/StructureDefinition/Patient";
|
||||||
|
|
||||||
|
var result = myValidationChain.fetchStructureDefinition(nonVersionedUrl);
|
||||||
|
|
||||||
|
assertNotNull(result, "Chain should resolve non-versioned URL directly");
|
||||||
|
assertInstanceOf(StructureDefinition.class, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testChainFetchResource() {
|
||||||
|
String versionedUrl = "http://hl7.org/fhir/StructureDefinition/Encounter|4.0.1";
|
||||||
|
|
||||||
|
var result = myValidationChain.fetchResource(StructureDefinition.class, versionedUrl);
|
||||||
|
|
||||||
|
assertNotNull(result, "fetchResource should work through chain with fallback");
|
||||||
|
assertEquals("http://hl7.org/fhir/StructureDefinition/Encounter", result.getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMultipleResourceTypes() {
|
||||||
|
// Verify fallback works for various resource types
|
||||||
|
String[] versionedUrls = {
|
||||||
|
"http://hl7.org/fhir/StructureDefinition/Condition|4.0.1",
|
||||||
|
"http://hl7.org/fhir/StructureDefinition/Medication|4.0.1",
|
||||||
|
"http://hl7.org/fhir/StructureDefinition/DiagnosticReport|4.0.1"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String versionedUrl : versionedUrls) {
|
||||||
|
var result = myValidationChain.fetchStructureDefinition(versionedUrl);
|
||||||
|
assertNotNull(result, "Should resolve " + versionedUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full validation integration tests using FhirInstanceValidator to prove
|
||||||
|
* the fallback support works in actual resource validation scenarios.
|
||||||
|
*/
|
||||||
|
@Nested
|
||||||
|
class WithFhirInstanceValidator {
|
||||||
|
|
||||||
|
private FhirContext myFhirContext;
|
||||||
|
private FhirValidator myValidator;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
myFhirContext = FhirContext.forR4Cached();
|
||||||
|
DefaultProfileValidationSupport defaultSupport = new DefaultProfileValidationSupport(myFhirContext);
|
||||||
|
InMemoryTerminologyServerValidationSupport terminologySupport =
|
||||||
|
new InMemoryTerminologyServerValidationSupport(myFhirContext);
|
||||||
|
CommonCodeSystemsTerminologyService commonCodeSystems =
|
||||||
|
new CommonCodeSystemsTerminologyService(myFhirContext);
|
||||||
|
|
||||||
|
// Build production-like validation chain with fallback support
|
||||||
|
ValidationSupportChain baseChain = new ValidationSupportChain(
|
||||||
|
defaultSupport,
|
||||||
|
terminologySupport,
|
||||||
|
commonCodeSystems
|
||||||
|
);
|
||||||
|
|
||||||
|
VersionedUrlFallbackValidationSupport fallbackSupport =
|
||||||
|
new VersionedUrlFallbackValidationSupport(myFhirContext, baseChain);
|
||||||
|
|
||||||
|
// ValidationSupportChain now handles caching internally (since HAPI FHIR 8.0.0)
|
||||||
|
ValidationSupportChain fullChain = new ValidationSupportChain(
|
||||||
|
fallbackSupport,
|
||||||
|
defaultSupport,
|
||||||
|
terminologySupport,
|
||||||
|
commonCodeSystems
|
||||||
|
);
|
||||||
|
|
||||||
|
FhirInstanceValidator instanceValidator = new FhirInstanceValidator(fullChain);
|
||||||
|
myValidator = myFhirContext.newValidator();
|
||||||
|
myValidator.registerValidatorModule(instanceValidator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testValidateSimpleObservation() {
|
||||||
|
Observation observation = new Observation();
|
||||||
|
observation.setStatus(Observation.ObservationStatus.FINAL);
|
||||||
|
observation.getCode().addCoding()
|
||||||
|
.setSystem("http://loinc.org")
|
||||||
|
.setCode("12345-6")
|
||||||
|
.setDisplay("Test");
|
||||||
|
|
||||||
|
ValidationResult result = myValidator.validateWithResult(observation);
|
||||||
|
|
||||||
|
// The validation should complete without errors related to unresolved versioned URLs
|
||||||
|
assertNotNull(result);
|
||||||
|
// We don't require the resource to be fully valid (may have other issues)
|
||||||
|
// but it should not fail due to missing versioned profile resolution
|
||||||
|
assertTrue(result.getMessages().stream()
|
||||||
|
.noneMatch(m -> m.getMessage().contains("Unable to locate profile")),
|
||||||
|
"Should not have profile resolution errors");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user