Merge remote-tracking branch 'origin/master' into rel_8_1_tracking

This commit is contained in:
dotasek
2025-05-16 11:24:32 -04:00
9 changed files with 189 additions and 23 deletions

View File

@@ -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.

View File

@@ -106,6 +106,8 @@ public class AppProperties {
private Integer pre_expand_value_sets_max_count = 1000;
private Integer maximum_expansion_size = 1000;
private Map<String, RemoteSystem> remote_terminology_service = null;
public List<String> getCustomInterceptorClasses() {
return custom_interceptor_classes;
}
@@ -723,6 +725,14 @@ public class AppProperties {
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 {
private Boolean allow_Credentials = true;
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 {
private Boolean resthook_enabled = false;

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

@@ -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;
}
}

View File

@@ -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<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
properties.putIfAbsent(AvailableSettings.SCANNER, "org.hibernate.boot.archive.scan.internal.DisabledScanner");
properties.putIfAbsent(

View File

@@ -293,6 +293,18 @@ hapi:
# max_page_size: 200
# retain_cached_searches_mins: 60
# 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:
home:
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.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() {

View File

@@ -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() {

View File

@@ -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
> {%