Merge remote-tracking branch 'origin/master' into rel_8_1_tracking
This commit is contained in:
@@ -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.
|
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:
|
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
|
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.
|
||||||
|
|
||||||
2) classes will be instantiated via reflection if no matching Bean is found
|
|
||||||
|
|
||||||
## Adding custom operations(providers)
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,19 @@ public class EnvironmentHelper {
|
|||||||
properties.put(strippedKey, entry.getValue().toString());
|
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<String, Object> jpaPropsEnv = getPropertiesStartingWith(environment, "SPRING_JPA_PROPERTIES");
|
||||||
|
for (Map.Entry<String, Object> 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
|
// Spring Boot Autoconfiguration defaults
|
||||||
properties.putIfAbsent(AvailableSettings.SCANNER, "org.hibernate.boot.archive.scan.internal.DisabledScanner");
|
properties.putIfAbsent(AvailableSettings.SCANNER, "org.hibernate.boot.archive.scan.internal.DisabledScanner");
|
||||||
properties.putIfAbsent(
|
properties.putIfAbsent(
|
||||||
|
|||||||
@@ -293,6 +293,18 @@ 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
|
||||||
|
# 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/'
|
||||||
tester:
|
tester:
|
||||||
home:
|
home:
|
||||||
name: Local Tester
|
name: Local Tester
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,7 @@ import jakarta.websocket.ContainerProvider;
|
|||||||
import jakarta.websocket.Session;
|
import jakarta.websocket.Session;
|
||||||
import jakarta.websocket.WebSocketContainer;
|
import jakarta.websocket.WebSocketContainer;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.hl7.fhir.r5.model.Bundle;
|
import org.hl7.fhir.r5.model.*;
|
||||||
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.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
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",
|
"spring.datasource.url=jdbc:h2:mem:dbr5",
|
||||||
"hapi.fhir.fhir_version=r5",
|
"hapi.fhir.fhir_version=r5",
|
||||||
"hapi.fhir.cr_enabled=false",
|
"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 {
|
public class ExampleServerR5IT {
|
||||||
|
|
||||||
@@ -156,6 +153,22 @@ public class ExampleServerR5IT {
|
|||||||
ourClient.delete().resourceById(mySubscriptionId).execute();
|
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
|
@BeforeEach
|
||||||
void beforeEach() {
|
void beforeEach() {
|
||||||
|
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ GET http://{{host}}/fhir/Patient/{{batch_patient_id}}/$everything
|
|||||||
|
|
||||||
### Extended Operations - validate
|
### Extended Operations - validate
|
||||||
# https://hapifhir.io/hapi-fhir/docs/server_plain/rest_operations_operations.html
|
# 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
|
POST http://{{host}}/fhir/Patient/{{batch_patient_id}}/$validate
|
||||||
|
|
||||||
> {%
|
> {%
|
||||||
|
|||||||
Reference in New Issue
Block a user