diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java b/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java index 33c9115..0095b53 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java @@ -106,6 +106,8 @@ public class AppProperties { private Integer pre_expand_value_sets_max_count = 1000; private Integer maximum_expansion_size = 1000; + private Map remote_terminology_service = null; + public List getCustomInterceptorClasses() { return custom_interceptor_classes; } @@ -723,6 +725,14 @@ public class AppProperties { this.maximum_expansion_size = maximum_expansion_size; } + public Map getRemoteTerminologyServicesMap() { + return remote_terminology_service; + } + + public void setRemote_terminology_service(Map remote_terminology_service) { + this.remote_terminology_service = remote_terminology_service; + } + public static class Cors { private Boolean allow_Credentials = true; private List 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 { private Boolean resthook_enabled = false; diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4.java index 55dce56..0a4bd73 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4.java @@ -1,9 +1,18 @@ 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.starter.AppProperties; 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.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.Configuration; import org.springframework.context.annotation.Import; @@ -17,4 +26,53 @@ import org.springframework.context.annotation.Import; ElasticsearchConfig.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; + } +} diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/validation/OnRemoteTerminologyPresent.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/validation/OnRemoteTerminologyPresent.java new file mode 100644 index 0000000..bd11463 --- /dev/null +++ b/src/main/java/ca/uhn/fhir/jpa/starter/common/validation/OnRemoteTerminologyPresent.java @@ -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(); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index ae90091..e81556e 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -293,6 +293,16 @@ hapi: # max_page_size: 200 # retain_cached_searches_mins: 60 # 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: home: name: Local Tester diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java index 3a15b96..1aef83a 100644 --- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java +++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java @@ -18,17 +18,7 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.Bundle; -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.hl7.fhir.r4.model.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; 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.version=1.1.0", "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 // 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"}) + "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 { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExampleServerR4IT.class); private IGenericClient ourClient; @@ -358,6 +352,21 @@ class ExampleServerR4IT implements IServerSupport { 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 void beforeEach() {