From 9cf166e765415c8cb185ab0ac031a404c7a27618 Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Wed, 19 Mar 2025 13:46:51 +0100 Subject: [PATCH 01/11] Added support for hybrid remote terminology --- .../uhn/fhir/jpa/starter/AppProperties.java | 30 ++++++++++++++++++ .../starter/common/FhirServerConfigR4.java | 31 ++++++++++++++++++- .../OnRemoteTerminologyPresent.java | 20 ++++++++++++ src/main/resources/application.yaml | 7 +++++ 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ca/uhn/fhir/jpa/starter/common/validation/OnRemoteTerminologyPresent.java 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 25ad070..87b1aea 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("*"); @@ -918,6 +928,26 @@ public class AppProperties { request_tenant_partitioning_mode = theRequest_tenant_partitioning_mode; } } + 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 { 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..ca0ca8e 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,24 @@ 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) + { + theAppProperties.getRemoteTerminologyServicesMap().forEach((key, remoteSystem) -> { + theValidationSupport.addValidationSupport(0 , new RemoteTerminologyServiceValidationSupport(theFhirContext, remoteSystem.getUrl()){ + @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..03e444b --- /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 a0756d6..cd66379 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -289,6 +289,13 @@ hapi: # max_page_size: 200 # retain_cached_searches_mins: 60 # reuse_cached_search_results_millis: 60000 + remote_terminology_service: + snomed: + system: 'http://snomed.info/sct' + url: 'https://tx.fhir.org/r4/' + loinc: + system: 'http://loinc.org' + url: 'https://fhir.loinc.org/' tester: home: name: Local Tester From 5c9f88e1cf7757102bd309e655f1bee278f7f2f5 Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Wed, 19 Mar 2025 23:51:21 +0100 Subject: [PATCH 02/11] Adding support for '*' cornercase --- .../uhn/fhir/jpa/starter/AppProperties.java | 3 +- .../starter/common/FhirServerConfigR4.java | 66 +++++++++++++++---- .../OnRemoteTerminologyPresent.java | 4 +- src/main/resources/application.yaml | 5 +- 4 files changed, 61 insertions(+), 17 deletions(-) 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 87b1aea..504d4fc 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java @@ -928,7 +928,8 @@ public class AppProperties { request_tenant_partitioning_mode = theRequest_tenant_partitioning_mode; } } - public static class RemoteSystem{ + + public static class RemoteSystem { private String system; private String url; 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 ca0ca8e..ffc4c44 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 @@ -30,20 +30,60 @@ public class FhirServerConfigR4 { @Bean(name = "myHybridRemoteValidationSupportChain") @Conditional({OnR4Condition.class, OnRemoteTerminologyPresent.class}) - public IValidationSupport addRemoteValidation(ValidationSupportChain theValidationSupport, FhirContext theFhirContext, AppProperties theAppProperties) - { - theAppProperties.getRemoteTerminologyServicesMap().forEach((key, remoteSystem) -> { - theValidationSupport.addValidationSupport(0 , new RemoteTerminologyServiceValidationSupport(theFhirContext, remoteSystem.getUrl()){ - @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; - } - }); - }); + 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()) { + @Override + public CodeValidationResult validateCode( + ValidationSupportContext theValidationSupportContext, + ConceptValidationOptions theOptions, + String theCodeSystem, + String theCode, + String theDisplay, + String theValueSetUrl) { + return super.validateCode( + theValidationSupportContext, + theOptions, + theCodeSystem, + theCode, + theDisplay, + theValueSetUrl); + } + }); + 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 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 index 03e444b..bd11463 100644 --- 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 @@ -11,8 +11,8 @@ public class OnRemoteTerminologyPresent implements Condition { public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) { AppProperties config = Binder.get(conditionContext.getEnvironment()) - .bind("hapi.fhir", AppProperties.class) - .orElse(null); + .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 cd66379..5827556 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -290,12 +290,15 @@ hapi: # 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://fhir.loinc.org/' + url: 'https://hapi.fhir.org/baseR4/' tester: home: name: Local Tester From ec0af6cae0522a3bfdc947184c9fb8bd08618ee2 Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Wed, 26 Mar 2025 23:19:50 +0100 Subject: [PATCH 03/11] Added simple pass-through test for remote validation --- .../uhn/fhir/jpa/starter/ExampleServerR4IT.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 d8044d8..dc06e20 100644 --- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java +++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java @@ -21,6 +21,8 @@ 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.BooleanType; +import org.hl7.fhir.r4.model.UriType; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; @@ -30,6 +32,7 @@ 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.ValueSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -77,7 +80,10 @@ import static org.opencds.cqf.fhir.utility.r4.Parameters.stringPart; // 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=http://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 +364,13 @@ class ExampleServerR4IT implements IServerSupport { assertTrue(foundDobChange); } + @Test + void testValidateRemoteTerminology() { + Parameters result = 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) result.getParameterValue("result")).getValue()); + assertEquals("Myocardial infarction", ((StringType) result.getParameterValue("display")).getValue()); + } + @BeforeEach void beforeEach() { From 00a10777f6fe8f33386645ac92ee0796996f6dc3 Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Mon, 31 Mar 2025 23:17:29 +0200 Subject: [PATCH 04/11] Add test so verify hybrid --- .../starter/common/FhirServerConfigR4.java | 25 ++++---------- .../fhir/jpa/starter/ExampleServerR4IT.java | 33 +++++++++---------- 2 files changed, 22 insertions(+), 36 deletions(-) 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 ffc4c44..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 @@ -39,30 +39,19 @@ public class FhirServerConfigR4 { if (values.size() == 1 && "*".equalsIgnoreCase(values.iterator().next().getSystem())) { var remoteSystem = values.iterator().next(); theValidationSupport.addValidationSupport( - 0, new RemoteTerminologyServiceValidationSupport(theFhirContext, remoteSystem.getUrl()) { - @Override - public CodeValidationResult validateCode( - ValidationSupportContext theValidationSupportContext, - ConceptValidationOptions theOptions, - String theCodeSystem, - String theCode, - String theDisplay, - String theValueSetUrl) { - return super.validateCode( - theValidationSupportContext, - theOptions, - theCodeSystem, - theCode, - theDisplay, - theValueSetUrl); - } - }); + 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, 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 dc06e20..9ea850c 100644 --- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java +++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java @@ -19,20 +19,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.BooleanType; -import org.hl7.fhir.r4.model.UriType; -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.ValueSet; +import org.hl7.fhir.r4.model.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -77,12 +64,13 @@ 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", "hapi.fhir.remote_terminology_service.snomed.system=http://snomed.info/sct", - "hapi.fhir.remote_terminology_service.snomed.url=http://tx.fhir.org/r4", + "hapi.fhir.remote_terminology_service.snomed.url=http://tx.fhir.org/r4" }) class ExampleServerR4IT implements IServerSupport { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExampleServerR4IT.class); @@ -366,9 +354,18 @@ class ExampleServerR4IT implements IServerSupport { @Test void testValidateRemoteTerminology() { - Parameters result = 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) result.getParameterValue("result")).getValue()); - assertEquals("Myocardial infarction", ((StringType) result.getParameterValue("display")).getValue()); + + 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))/*.andParameter( "code", new StringType("yes"))*/.execute(); + assertEquals(true, ((BooleanType) localResult.getParameterValue("result")).getValue()); } @BeforeEach From b9f6e853ac3d103bfc80437eb00928f82412f87c Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Tue, 29 Apr 2025 12:08:42 +0200 Subject: [PATCH 05/11] Update src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 9ea850c..2779c35 100644 --- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java +++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java @@ -364,8 +364,7 @@ class ExampleServerR4IT implements IServerSupport { 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))/*.andParameter( "code", new StringType("yes"))*/.execute(); - assertEquals(true, ((BooleanType) localResult.getParameterValue("result")).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 From a7da4f21819748cb6a93ec30002bae0ba92bfd51 Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Tue, 29 Apr 2025 13:24:39 +0200 Subject: [PATCH 06/11] Added 's' --- src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1a915b4..1aef83a 100644 --- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java +++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java @@ -70,7 +70,7 @@ import static org.opencds.cqf.fhir.utility.r4.Parameters.stringPart; // when running in a spring boot environment "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=http://tx.fhir.org/r4" + "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); From 88ed318e7610897abbf7956b38ace7947bed2634 Mon Sep 17 00:00:00 2001 From: craig mcclendon Date: Tue, 6 May 2025 13:14:58 -0500 Subject: [PATCH 07/11] process jpa properties set as env variables (#808) * process jpa properties set as env variables * Update src/main/java/ca/uhn/fhir/jpa/starter/util/EnvironmentHelper.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Jens Kristian Villadsen Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../fhir/jpa/starter/util/EnvironmentHelper.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/util/EnvironmentHelper.java b/src/main/java/ca/uhn/fhir/jpa/starter/util/EnvironmentHelper.java index e62b089..e22a179 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/util/EnvironmentHelper.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/util/EnvironmentHelper.java @@ -40,6 +40,19 @@ public class EnvironmentHelper { properties.put(strippedKey, entry.getValue().toString()); } + // also check for JPA properties set as environment variables, this is slightly hacky and doesn't cover all + // the naming conventions Springboot allows + // but there doesn't seem to be a better/deterministic way to get these properties when they are set as ENV + // variables and this at least provides + // a way to set them (in a docker container, for instance) + Map jpaPropsEnv = getPropertiesStartingWith(environment, "SPRING_JPA_PROPERTIES"); + for (Map.Entry entry : jpaPropsEnv.entrySet()) { + String strippedKey = entry.getKey().replace("SPRING_JPA_PROPERTIES_", ""); + strippedKey = strippedKey.replaceAll("_", "."); + strippedKey = strippedKey.toLowerCase(); + properties.put(strippedKey, entry.getValue().toString()); + } + // Spring Boot Autoconfiguration defaults properties.putIfAbsent(AvailableSettings.SCANNER, "org.hibernate.boot.archive.scan.internal.DisabledScanner"); properties.putIfAbsent( From 6863e4c1e667afe042754b77c807457252fcb2a3 Mon Sep 17 00:00:00 2001 From: Patrick Werner Date: Wed, 7 May 2025 10:35:46 +0200 Subject: [PATCH 08/11] feat: add timeout comment for validate operation in plain_server.http (#817) --- src/test/smoketest/plain_server.http | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/smoketest/plain_server.http b/src/test/smoketest/plain_server.http index 572a48f..72b300a 100644 --- a/src/test/smoketest/plain_server.http +++ b/src/test/smoketest/plain_server.http @@ -206,6 +206,7 @@ GET http://{{host}}/fhir/Patient/{{batch_patient_id}}/$everything ### Extended Operations - validate # https://hapifhir.io/hapi-fhir/docs/server_plain/rest_operations_operations.html +# @timeout 180 # wait up to 2 minutes for the validate response POST http://{{host}}/fhir/Patient/{{batch_patient_id}}/$validate > {% From 5ee2e4796ed67240d4ef00dfa2c029ae895ce0e3 Mon Sep 17 00:00:00 2001 From: Martin Bernstorff Date: Sun, 11 May 2025 18:27:46 +0100 Subject: [PATCH 09/11] docs: document interceptor registration for non-rest calls (#814) --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ebca0d8..e88cfc0 100644 --- a/README.md +++ b/README.md @@ -357,11 +357,10 @@ It is recommended to deploy a case-sensitive database prior to running HAPI FHIR Custom interceptors can be registered with the server by including the property `hapi.fhir.custom-interceptor-classes`. This will take a comma separated list of fully-qualified class names which will be registered with the server. Interceptors will be discovered in one of two ways: -1) discovered from the Spring application context as existing Beans (can be used in conjunction with `hapi.fhir.custom-bean-packages`) or registered with Spring via other methods +1) From the Spring application context as existing Beans (can be used in conjunction with `hapi.fhir.custom-bean-packages`) or registered with Spring via other methods +2) Classes will be instantiated via reflection if no matching Bean is found -or - -2) classes will be instantiated via reflection if no matching Bean is found +Interceptors can also be registered manually through `RestfulServer.registerInterceptor`. Take note that any interceptor registered in this way _will not fire_ for non-REST operations, e.g. creation through a DAO. To trigger in this case, you need to register your interceptors on the `IInterceptorService` bean. ## Adding custom operations(providers) Custom operations(providers) can be registered with the server by including the property `hapi.fhir.custom-provider-classes`. This will take a comma separated list of fully-qualified class names which will be registered with the server. From ba0589ce7750d2392a36031f7bcd07d3beac658b Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Tue, 13 May 2025 15:39:09 +0200 Subject: [PATCH 10/11] Moved terminology config out of being R4 specific. It now applies to all versions (#822) --- .../starter/common/FhirServerConfigR4.java | 60 +--------------- .../terminology/TerminologyConfig.java | 68 +++++++++++++++++++ .../fhir/jpa/starter/ExampleServerR5IT.java | 27 ++++++-- 3 files changed, 89 insertions(+), 66 deletions(-) create mode 100644 src/main/java/ca/uhn/fhir/jpa/starter/terminology/TerminologyConfig.java 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 0a4bd73..55dce56 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,18 +1,9 @@ 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; @@ -26,53 +17,4 @@ import org.springframework.context.annotation.Import; ElasticsearchConfig.class, StarterIpsConfig.class }) -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; - } -} +public class FhirServerConfigR4 {} diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/terminology/TerminologyConfig.java b/src/main/java/ca/uhn/fhir/jpa/starter/terminology/TerminologyConfig.java new file mode 100644 index 0000000..9b61dde --- /dev/null +++ b/src/main/java/ca/uhn/fhir/jpa/starter/terminology/TerminologyConfig.java @@ -0,0 +1,68 @@ +package ca.uhn.fhir.jpa.starter.terminology; + +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.starter.AppProperties; +import ca.uhn.fhir.jpa.starter.common.StarterJpaConfig; +import ca.uhn.fhir.jpa.starter.common.validation.OnRemoteTerminologyPresent; +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; + +@Configuration +@Conditional(OnRemoteTerminologyPresent.class) +@Import(StarterJpaConfig.class) +public class TerminologyConfig { + + @Bean(name = "myHybridRemoteValidationSupportChain") + 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/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR5IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR5IT.java index e26ad37..f31d003 100644 --- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR5IT.java +++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR5IT.java @@ -11,12 +11,7 @@ import jakarta.websocket.ContainerProvider; import jakarta.websocket.Session; import jakarta.websocket.WebSocketContainer; import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r5.model.Bundle; -import org.hl7.fhir.r5.model.Enumerations; -import org.hl7.fhir.r5.model.Observation; -import org.hl7.fhir.r5.model.Patient; -import org.hl7.fhir.r5.model.Subscription; -import org.hl7.fhir.r5.model.SubscriptionTopic; +import org.hl7.fhir.r5.model.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -36,7 +31,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; "spring.datasource.url=jdbc:h2:mem:dbr5", "hapi.fhir.fhir_version=r5", "hapi.fhir.cr_enabled=false", - "hapi.fhir.subscription.websocket_enabled=true" + "hapi.fhir.subscription.websocket_enabled=true", + "hapi.fhir.remote_terminology_service.snomed.system=http://snomed.info/sct", + "hapi.fhir.remote_terminology_service.snomed.url=https://tx.fhir.org/r5" }) public class ExampleServerR5IT { @@ -156,6 +153,22 @@ public class ExampleServerR5IT { ourClient.delete().resourceById(mySubscriptionId).execute(); } + + @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() { From f2c0d7709346683ea747efb6e2e66ef673e48dd7 Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Wed, 14 May 2025 11:08:27 +0200 Subject: [PATCH 11/11] Removing accidently enabled remote terminology by default (#819) * Removing accidently enabled remote terminology by default * Update src/main/resources/application.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main/resources/application.yaml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index e81556e..d4df357 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -293,16 +293,18 @@ hapi: # max_page_size: 200 # retain_cached_searches_mins: 60 # reuse_cached_search_results_millis: 60000 - remote_terminology_service: + # The remote_terminology_service block is commented out by default because it requires external terminology service endpoints. + # Uncomment and configure the block below if you need to enable remote terminology validation or mapping. + #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/' + # 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