Skip to content

Commit a3a2416

Browse files
committed
Merge remote-tracking branch 'origin/master' into rel_8_1_tracking
2 parents 573b841 + f2c0d77 commit a3a2416

File tree

9 files changed

+189
-23
lines changed

9 files changed

+189
-23
lines changed

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,11 +357,10 @@ It is recommended to deploy a case-sensitive database prior to running HAPI FHIR
357357
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.
358358
Interceptors will be discovered in one of two ways:
359359

360-
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
361-
362-
or
360+
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
361+
2) Classes will be instantiated via reflection if no matching Bean is found
363362

364-
2) classes will be instantiated via reflection if no matching Bean is found
363+
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.
365364

366365
## Adding custom operations(providers)
367366
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.

src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ public class AppProperties {
106106
private Integer pre_expand_value_sets_max_count = 1000;
107107
private Integer maximum_expansion_size = 1000;
108108

109+
private Map<String, RemoteSystem> remote_terminology_service = null;
110+
109111
public List<String> getCustomInterceptorClasses() {
110112
return custom_interceptor_classes;
111113
}
@@ -723,6 +725,14 @@ public void setMaximum_expansion_size(Integer maximum_expansion_size) {
723725
this.maximum_expansion_size = maximum_expansion_size;
724726
}
725727

728+
public Map<String, RemoteSystem> getRemoteTerminologyServicesMap() {
729+
return remote_terminology_service;
730+
}
731+
732+
public void setRemote_terminology_service(Map<String, RemoteSystem> remote_terminology_service) {
733+
this.remote_terminology_service = remote_terminology_service;
734+
}
735+
726736
public static class Cors {
727737
private Boolean allow_Credentials = true;
728738
private List<String> allowed_origin = List.of("*");
@@ -919,6 +929,27 @@ public void setRequest_tenant_partitioning_mode(boolean theRequest_tenant_partit
919929
}
920930
}
921931

932+
public static class RemoteSystem {
933+
private String system;
934+
private String url;
935+
936+
public String getSystem() {
937+
return system;
938+
}
939+
940+
public void setSystem(String system) {
941+
this.system = system;
942+
}
943+
944+
public String getUrl() {
945+
return url;
946+
}
947+
948+
public void setUrl(String url) {
949+
this.url = url;
950+
}
951+
}
952+
922953
public static class Subscription {
923954

924955
private Boolean resthook_enabled = false;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package ca.uhn.fhir.jpa.starter.common.validation;
2+
3+
import ca.uhn.fhir.jpa.starter.AppProperties;
4+
import org.springframework.boot.context.properties.bind.Binder;
5+
import org.springframework.context.annotation.Condition;
6+
import org.springframework.context.annotation.ConditionContext;
7+
import org.springframework.core.type.AnnotatedTypeMetadata;
8+
9+
public class OnRemoteTerminologyPresent implements Condition {
10+
@Override
11+
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) {
12+
13+
AppProperties config = Binder.get(conditionContext.getEnvironment())
14+
.bind("hapi.fhir", AppProperties.class)
15+
.orElse(null);
16+
if (config == null) return false;
17+
if (config.getRemoteTerminologyServicesMap() == null) return false;
18+
return !config.getRemoteTerminologyServicesMap().isEmpty();
19+
}
20+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package ca.uhn.fhir.jpa.starter.terminology;
2+
3+
import ca.uhn.fhir.context.FhirContext;
4+
import ca.uhn.fhir.context.support.ConceptValidationOptions;
5+
import ca.uhn.fhir.context.support.IValidationSupport;
6+
import ca.uhn.fhir.context.support.ValidationSupportContext;
7+
import ca.uhn.fhir.jpa.starter.AppProperties;
8+
import ca.uhn.fhir.jpa.starter.common.StarterJpaConfig;
9+
import ca.uhn.fhir.jpa.starter.common.validation.OnRemoteTerminologyPresent;
10+
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
11+
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
12+
import org.springframework.context.annotation.Bean;
13+
import org.springframework.context.annotation.Conditional;
14+
import org.springframework.context.annotation.Configuration;
15+
import org.springframework.context.annotation.Import;
16+
17+
@Configuration
18+
@Conditional(OnRemoteTerminologyPresent.class)
19+
@Import(StarterJpaConfig.class)
20+
public class TerminologyConfig {
21+
22+
@Bean(name = "myHybridRemoteValidationSupportChain")
23+
public IValidationSupport addRemoteValidation(
24+
ValidationSupportChain theValidationSupport, FhirContext theFhirContext, AppProperties theAppProperties) {
25+
var values = theAppProperties.getRemoteTerminologyServicesMap().values();
26+
27+
// If the remote terminology service is "*" and is the only one then forward all requests to the remote
28+
// terminology service
29+
if (values.size() == 1 && "*".equalsIgnoreCase(values.iterator().next().getSystem())) {
30+
var remoteSystem = values.iterator().next();
31+
theValidationSupport.addValidationSupport(
32+
0, new RemoteTerminologyServiceValidationSupport(theFhirContext, remoteSystem.getUrl()));
33+
return theValidationSupport;
34+
35+
// If there are multiple remote terminology services, then add each one to the validation chain
36+
} else {
37+
values.forEach((remoteSystem) -> theValidationSupport.addValidationSupport(
38+
0, new RemoteTerminologyServiceValidationSupport(theFhirContext, remoteSystem.getUrl()) {
39+
@Override
40+
public boolean isCodeSystemSupported(
41+
ValidationSupportContext theValidationSupportContext, String theSystem) {
42+
return remoteSystem.getSystem().equalsIgnoreCase(theSystem);
43+
}
44+
45+
@Override
46+
public CodeValidationResult validateCode(
47+
ValidationSupportContext theValidationSupportContext,
48+
ConceptValidationOptions theOptions,
49+
String theCodeSystem,
50+
String theCode,
51+
String theDisplay,
52+
String theValueSetUrl) {
53+
if (remoteSystem.getSystem().equalsIgnoreCase(theCodeSystem)) {
54+
return super.validateCode(
55+
theValidationSupportContext,
56+
theOptions,
57+
theCodeSystem,
58+
theCode,
59+
theDisplay,
60+
theValueSetUrl);
61+
}
62+
return null;
63+
}
64+
}));
65+
}
66+
return theValidationSupport;
67+
}
68+
}

src/main/java/ca/uhn/fhir/jpa/starter/util/EnvironmentHelper.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ public static Properties getHibernateProperties(
4040
properties.put(strippedKey, entry.getValue().toString());
4141
}
4242

43+
// also check for JPA properties set as environment variables, this is slightly hacky and doesn't cover all
44+
// the naming conventions Springboot allows
45+
// but there doesn't seem to be a better/deterministic way to get these properties when they are set as ENV
46+
// variables and this at least provides
47+
// a way to set them (in a docker container, for instance)
48+
Map<String, Object> jpaPropsEnv = getPropertiesStartingWith(environment, "SPRING_JPA_PROPERTIES");
49+
for (Map.Entry<String, Object> entry : jpaPropsEnv.entrySet()) {
50+
String strippedKey = entry.getKey().replace("SPRING_JPA_PROPERTIES_", "");
51+
strippedKey = strippedKey.replaceAll("_", ".");
52+
strippedKey = strippedKey.toLowerCase();
53+
properties.put(strippedKey, entry.getValue().toString());
54+
}
55+
4356
// Spring Boot Autoconfiguration defaults
4457
properties.putIfAbsent(AvailableSettings.SCANNER, "org.hibernate.boot.archive.scan.internal.DisabledScanner");
4558
properties.putIfAbsent(

src/main/resources/application.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,18 @@ hapi:
293293
# max_page_size: 200
294294
# retain_cached_searches_mins: 60
295295
# reuse_cached_search_results_millis: 60000
296+
# The remote_terminology_service block is commented out by default because it requires external terminology service endpoints.
297+
# Uncomment and configure the block below if you need to enable remote terminology validation or mapping.
298+
#remote_terminology_service:
299+
# all:
300+
# system: '*'
301+
# url: 'https://tx.fhir.org/r4/'
302+
# snomed:
303+
# system: 'http://snomed.info/sct'
304+
# url: 'https://tx.fhir.org/r4/'
305+
# loinc:
306+
# system: 'http://loinc.org'
307+
# url: 'https://hapi.fhir.org/baseR4/'
296308
tester:
297309
home:
298310
name: Local Tester

src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,7 @@
1818
import org.apache.http.impl.client.HttpClients;
1919
import org.hl7.fhir.instance.model.api.IBaseResource;
2020
import org.hl7.fhir.instance.model.api.IIdType;
21-
import org.hl7.fhir.r4.model.Bundle;
22-
import org.hl7.fhir.r4.model.DateType;
23-
import org.hl7.fhir.r4.model.IdType;
24-
import org.hl7.fhir.r4.model.Measure;
25-
import org.hl7.fhir.r4.model.MeasureReport;
26-
import org.hl7.fhir.r4.model.Observation;
27-
import org.hl7.fhir.r4.model.Parameters;
28-
import org.hl7.fhir.r4.model.Patient;
29-
import org.hl7.fhir.r4.model.Period;
30-
import org.hl7.fhir.r4.model.StringType;
31-
import org.hl7.fhir.r4.model.Subscription;
21+
import org.hl7.fhir.r4.model.*;
3222
import org.junit.jupiter.api.BeforeEach;
3323
import org.junit.jupiter.api.Order;
3424
import org.junit.jupiter.api.Test;
@@ -74,10 +64,14 @@
7464
"hapi.fhir.implementationguides.dk-core.name=hl7.fhir.dk.core",
7565
"hapi.fhir.implementationguides.dk-core.version=1.1.0",
7666
"hapi.fhir.auto_create_placeholder_reference_targets=true",
67+
"hibernate.search.enabled=true",
7768
// Override is currently required when using MDM as the construction of the MDM
7869
// beans are ambiguous as they are constructed multiple places. This is evident
7970
// when running in a spring boot environment
80-
"spring.main.allow-bean-definition-overriding=true"})
71+
"spring.main.allow-bean-definition-overriding=true",
72+
"hapi.fhir.remote_terminology_service.snomed.system=http://snomed.info/sct",
73+
"hapi.fhir.remote_terminology_service.snomed.url=https://tx.fhir.org/r4"
74+
})
8175
class ExampleServerR4IT implements IServerSupport {
8276
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExampleServerR4IT.class);
8377
private IGenericClient ourClient;
@@ -358,6 +352,21 @@ void testDiffOperationIsRegistered() {
358352
assertTrue(foundDobChange);
359353
}
360354

355+
@Test
356+
void testValidateRemoteTerminology() {
357+
358+
String testCodeSystem = "http://foo/cs";
359+
String testValueSet = "http://foo/vs";
360+
ourClient.create().resource(new CodeSystem().setUrl(testCodeSystem).addConcept(new CodeSystem.ConceptDefinitionComponent().setCode("yes")).addConcept(new CodeSystem.ConceptDefinitionComponent().setCode("no"))).execute();
361+
ourClient.create().resource(new ValueSet().setUrl(testValueSet).setCompose(new ValueSet.ValueSetComposeComponent().addInclude(new ValueSet.ConceptSetComponent().setSystem(testValueSet)))).execute();
362+
363+
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();
364+
assertEquals(true, ((BooleanType) remoteResult.getParameterValue("result")).getValue());
365+
assertEquals("Myocardial infarction", ((StringType) remoteResult.getParameterValue("display")).getValue());
366+
367+
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();
368+
}
369+
361370
@BeforeEach
362371
void beforeEach() {
363372

src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR5IT.java

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,7 @@
1111
import jakarta.websocket.Session;
1212
import jakarta.websocket.WebSocketContainer;
1313
import org.hl7.fhir.instance.model.api.IIdType;
14-
import org.hl7.fhir.r5.model.Bundle;
15-
import org.hl7.fhir.r5.model.Enumerations;
16-
import org.hl7.fhir.r5.model.Observation;
17-
import org.hl7.fhir.r5.model.Patient;
18-
import org.hl7.fhir.r5.model.Subscription;
19-
import org.hl7.fhir.r5.model.SubscriptionTopic;
14+
import org.hl7.fhir.r5.model.*;
2015
import org.junit.jupiter.api.BeforeEach;
2116
import org.junit.jupiter.api.Test;
2217
import org.junit.jupiter.api.extension.ExtendWith;
@@ -36,7 +31,9 @@
3631
"spring.datasource.url=jdbc:h2:mem:dbr5",
3732
"hapi.fhir.fhir_version=r5",
3833
"hapi.fhir.cr_enabled=false",
39-
"hapi.fhir.subscription.websocket_enabled=true"
34+
"hapi.fhir.subscription.websocket_enabled=true",
35+
"hapi.fhir.remote_terminology_service.snomed.system=http://snomed.info/sct",
36+
"hapi.fhir.remote_terminology_service.snomed.url=https://tx.fhir.org/r5"
4037
})
4138
public class ExampleServerR5IT {
4239

@@ -156,6 +153,22 @@ void testWebsocketSubscription() throws Exception {
156153
ourClient.delete().resourceById(mySubscriptionId).execute();
157154
}
158155

156+
157+
@Test
158+
void testValidateRemoteTerminology() {
159+
160+
String testCodeSystem = "http://foo/cs";
161+
String testValueSet = "http://foo/vs";
162+
ourClient.create().resource(new CodeSystem().setUrl(testCodeSystem).addConcept(new CodeSystem.ConceptDefinitionComponent().setCode("yes")).addConcept(new CodeSystem.ConceptDefinitionComponent().setCode("no"))).execute();
163+
ourClient.create().resource(new ValueSet().setUrl(testValueSet).setCompose(new ValueSet.ValueSetComposeComponent().addInclude(new ValueSet.ConceptSetComponent().setSystem(testValueSet)))).execute();
164+
165+
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();
166+
assertEquals(true, ((BooleanType) remoteResult.getParameterValue("result")).getValue());
167+
assertEquals("Myocardial infarction", ((StringType) remoteResult.getParameterValue("display")).getValue());
168+
169+
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();
170+
}
171+
159172
@BeforeEach
160173
void beforeEach() {
161174

src/test/smoketest/plain_server.http

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ GET http://{{host}}/fhir/Patient/{{batch_patient_id}}/$everything
206206

207207
### Extended Operations - validate
208208
# https://hapifhir.io/hapi-fhir/docs/server_plain/rest_operations_operations.html
209+
# @timeout 180 # wait up to 2 minutes for the validate response
209210
POST http://{{host}}/fhir/Patient/{{batch_patient_id}}/$validate
210211

211212
> {%

0 commit comments

Comments
 (0)