Merge pull request #795 from jkiddo/feat/remote-terminology-support

Added support for hybrid remote terminology
This commit is contained in:
Patrick Werner
2025-05-07 11:15:01 +02:00
committed by GitHub
5 changed files with 141 additions and 13 deletions

View File

@@ -106,6 +106,8 @@ public class AppProperties {
private Integer pre_expand_value_sets_max_count = 1000; private Integer pre_expand_value_sets_max_count = 1000;
private Integer maximum_expansion_size = 1000; private Integer maximum_expansion_size = 1000;
private Map<String, RemoteSystem> remote_terminology_service = null;
public List<String> getCustomInterceptorClasses() { public List<String> getCustomInterceptorClasses() {
return custom_interceptor_classes; return custom_interceptor_classes;
} }
@@ -723,6 +725,14 @@ public class AppProperties {
this.maximum_expansion_size = maximum_expansion_size; this.maximum_expansion_size = maximum_expansion_size;
} }
public Map<String, RemoteSystem> getRemoteTerminologyServicesMap() {
return remote_terminology_service;
}
public void setRemote_terminology_service(Map<String, RemoteSystem> remote_terminology_service) {
this.remote_terminology_service = remote_terminology_service;
}
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("*");
@@ -919,6 +929,27 @@ public class AppProperties {
} }
} }
public static class RemoteSystem {
private String system;
private String url;
public String getSystem() {
return system;
}
public void setSystem(String system) {
this.system = system;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
public static class Subscription { public static class Subscription {
private Boolean resthook_enabled = false; private Boolean resthook_enabled = false;

View File

@@ -1,9 +1,18 @@
package ca.uhn.fhir.jpa.starter.common; package ca.uhn.fhir.jpa.starter.common;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.ConceptValidationOptions;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.jpa.config.r4.JpaR4Config; import ca.uhn.fhir.jpa.config.r4.JpaR4Config;
import ca.uhn.fhir.jpa.starter.AppProperties;
import ca.uhn.fhir.jpa.starter.annotations.OnR4Condition; import ca.uhn.fhir.jpa.starter.annotations.OnR4Condition;
import ca.uhn.fhir.jpa.starter.common.validation.OnRemoteTerminologyPresent;
import ca.uhn.fhir.jpa.starter.cr.StarterCrR4Config; import ca.uhn.fhir.jpa.starter.cr.StarterCrR4Config;
import ca.uhn.fhir.jpa.starter.ips.StarterIpsConfig; import ca.uhn.fhir.jpa.starter.ips.StarterIpsConfig;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
@@ -17,4 +26,53 @@ import org.springframework.context.annotation.Import;
ElasticsearchConfig.class, ElasticsearchConfig.class,
StarterIpsConfig.class StarterIpsConfig.class
}) })
public class FhirServerConfigR4 {} public class FhirServerConfigR4 {
@Bean(name = "myHybridRemoteValidationSupportChain")
@Conditional({OnR4Condition.class, OnRemoteTerminologyPresent.class})
public IValidationSupport addRemoteValidation(
ValidationSupportChain theValidationSupport, FhirContext theFhirContext, AppProperties theAppProperties) {
var values = theAppProperties.getRemoteTerminologyServicesMap().values();
// If the remote terminology service is "*" and is the only one then forward all requests to the remote
// terminology service
if (values.size() == 1 && "*".equalsIgnoreCase(values.iterator().next().getSystem())) {
var remoteSystem = values.iterator().next();
theValidationSupport.addValidationSupport(
0, new RemoteTerminologyServiceValidationSupport(theFhirContext, remoteSystem.getUrl()));
return theValidationSupport;
// If there are multiple remote terminology services, then add each one to the validation chain
} else {
values.forEach((remoteSystem) -> theValidationSupport.addValidationSupport(
0, new RemoteTerminologyServiceValidationSupport(theFhirContext, remoteSystem.getUrl()) {
@Override
public boolean isCodeSystemSupported(
ValidationSupportContext theValidationSupportContext, String theSystem) {
return remoteSystem.getSystem().equalsIgnoreCase(theSystem);
}
@Override
public CodeValidationResult validateCode(
ValidationSupportContext theValidationSupportContext,
ConceptValidationOptions theOptions,
String theCodeSystem,
String theCode,
String theDisplay,
String theValueSetUrl) {
if (remoteSystem.getSystem().equalsIgnoreCase(theCodeSystem)) {
return super.validateCode(
theValidationSupportContext,
theOptions,
theCodeSystem,
theCode,
theDisplay,
theValueSetUrl);
}
return null;
}
}));
}
return theValidationSupport;
}
}

View File

@@ -0,0 +1,20 @@
package ca.uhn.fhir.jpa.starter.common.validation;
import ca.uhn.fhir.jpa.starter.AppProperties;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class OnRemoteTerminologyPresent implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) {
AppProperties config = Binder.get(conditionContext.getEnvironment())
.bind("hapi.fhir", AppProperties.class)
.orElse(null);
if (config == null) return false;
if (config.getRemoteTerminologyServicesMap() == null) return false;
return !config.getRemoteTerminologyServicesMap().isEmpty();
}
}

View File

@@ -293,6 +293,16 @@ hapi:
# max_page_size: 200 # max_page_size: 200
# retain_cached_searches_mins: 60 # retain_cached_searches_mins: 60
# reuse_cached_search_results_millis: 60000 # reuse_cached_search_results_millis: 60000
remote_terminology_service:
# all:
# system: '*'
# url: 'https://tx.fhir.org/r4/'
snomed:
system: 'http://snomed.info/sct'
url: 'https://tx.fhir.org/r4/'
loinc:
system: 'http://loinc.org'
url: 'https://hapi.fhir.org/baseR4/'
tester: tester:
home: home:
name: Local Tester name: Local Tester

View File

@@ -18,17 +18,7 @@ import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.client.HttpClients;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Period;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.Subscription;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -74,10 +64,14 @@ import static org.opencds.cqf.fhir.utility.r4.Parameters.stringPart;
"hapi.fhir.implementationguides.dk-core.name=hl7.fhir.dk.core", "hapi.fhir.implementationguides.dk-core.name=hl7.fhir.dk.core",
"hapi.fhir.implementationguides.dk-core.version=1.1.0", "hapi.fhir.implementationguides.dk-core.version=1.1.0",
"hapi.fhir.auto_create_placeholder_reference_targets=true", "hapi.fhir.auto_create_placeholder_reference_targets=true",
"hibernate.search.enabled=true",
// Override is currently required when using MDM as the construction of the MDM // 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 // beans are ambiguous as they are constructed multiple places. This is evident
// when running in a spring boot environment // when running in a spring boot environment
"spring.main.allow-bean-definition-overriding=true"}) "spring.main.allow-bean-definition-overriding=true",
"hapi.fhir.remote_terminology_service.snomed.system=http://snomed.info/sct",
"hapi.fhir.remote_terminology_service.snomed.url=https://tx.fhir.org/r4"
})
class ExampleServerR4IT implements IServerSupport { class ExampleServerR4IT implements IServerSupport {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExampleServerR4IT.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExampleServerR4IT.class);
private IGenericClient ourClient; private IGenericClient ourClient;
@@ -358,6 +352,21 @@ class ExampleServerR4IT implements IServerSupport {
assertTrue(foundDobChange); assertTrue(foundDobChange);
} }
@Test
void testValidateRemoteTerminology() {
String testCodeSystem = "http://foo/cs";
String testValueSet = "http://foo/vs";
ourClient.create().resource(new CodeSystem().setUrl(testCodeSystem).addConcept(new CodeSystem.ConceptDefinitionComponent().setCode("yes")).addConcept(new CodeSystem.ConceptDefinitionComponent().setCode("no"))).execute();
ourClient.create().resource(new ValueSet().setUrl(testValueSet).setCompose(new ValueSet.ValueSetComposeComponent().addInclude(new ValueSet.ConceptSetComponent().setSystem(testValueSet)))).execute();
Parameters remoteResult = ourClient.operation().onType(ValueSet.class).named("$validate-code").withParameter(Parameters.class, "code", new StringType("22298006")).andParameter("system", new UriType("http://snomed.info/sct")).execute();
assertEquals(true, ((BooleanType) remoteResult.getParameterValue("result")).getValue());
assertEquals("Myocardial infarction", ((StringType) remoteResult.getParameterValue("display")).getValue());
Parameters localResult = ourClient.operation().onType(CodeSystem.class).named("$validate-code").withParameter(Parameters.class, "url", new UrlType(testCodeSystem)).andParameter("coding", new Coding(testCodeSystem, "yes", null)).execute();
}
@BeforeEach @BeforeEach
void beforeEach() { void beforeEach() {