Remote terminology service enhancements (#1934)

* Remote terminology service enhancements

* Add changelog
This commit is contained in:
James Agnew 2020-06-23 11:35:26 -04:00 committed by GitHub
parent 67d363f9e1
commit f88298a1fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 283 additions and 57 deletions

View File

@ -31,6 +31,7 @@ import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@ -327,13 +328,7 @@ public class DefaultProfileValidationSupport implements IValidationSupport {
}
private String getConformanceResourceUrl(IBaseResource theResource) {
String urlValueString = null;
Optional<IBase> urlValue = getFhirContext().getResourceDefinition(theResource).getChildByName("url").getAccessor().getFirstValueOrNull(theResource);
if (urlValue.isPresent()) {
IPrimitiveType<?> urlValueType = (IPrimitiveType<?>) urlValue.get();
urlValueString = urlValueType.getValueAsString();
}
return urlValueString;
return getConformanceResourceUrl(getFhirContext(), theResource);
}
private List<IBaseResource> parseBundle(InputStreamReader theReader) {
@ -346,6 +341,17 @@ public class DefaultProfileValidationSupport implements IValidationSupport {
}
}
@Nullable
public static String getConformanceResourceUrl(FhirContext theFhirContext, IBaseResource theResource) {
String urlValueString = null;
Optional<IBase> urlValue = theFhirContext.getResourceDefinition(theResource).getChildByName("url").getAccessor().getFirstValueOrNull(theResource);
if (urlValue.isPresent()) {
IPrimitiveType<?> urlValueType = (IPrimitiveType<?>) urlValue.get();
urlValueString = urlValueType.getValueAsString();
}
return urlValueString;
}
static <T extends IBaseResource> List<T> toList(Map<String, IBaseResource> theMap) {
ArrayList<IBaseResource> retVal = new ArrayList<>(theMap.values());
return (List<T>) Collections.unmodifiableList(retVal);

View File

@ -0,0 +1,8 @@
---
type: add
issue: 1934
title: "The **RemoteTerminologyServiceValidationSupport** validation support module, which is used to connect to
external/remote terminology services, has been significantly enhanced to provide testing for supported
CodeSystems and ValueSets. It will also now validate codes in fields that are not bound to a specific
ValueSet."

View File

@ -118,6 +118,12 @@ The following table lists vocabulary that is validated by this module:
This module validates codes using a remote FHIR-based terminology server.
This module will invoke the following operations on the remote terminology server:
* **GET [base]/CodeSystem?url=[url]** &ndash; Tests whether a given CodeSystem is supported on the server
* **GET [base]/ValueSet?url=[url]** &ndash; Tests whether a given ValueSet is supported on the server
* **POST [base]/CodeSystem/$validate-code** &ndash; Validate codes in fields where no specific ValueSet is bound
* **POST [base]/ValueSet/$validate-code** &ndash; Validate codes in fields where a specific ValueSet is bound
# Recipes

View File

@ -2,13 +2,17 @@ package org.hl7.fhir.common.hapi.validation.support;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.ConceptValidationOptions;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.ParametersUtil;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.CodeSystem;
import javax.annotation.Nonnull;
import java.util.ArrayList;
@ -42,6 +46,64 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, theValueSetUrl, null);
}
@Override
public CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
IBaseResource valueSet = theValueSet;
String valueSetUrl = DefaultProfileValidationSupport.getConformanceResourceUrl(myCtx, valueSet);
if (isNotBlank(valueSetUrl)) {
valueSet = null;
} else {
valueSetUrl = null;
}
return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, valueSetUrl, valueSet);
}
@Override
public IBaseResource fetchCodeSystem(String theSystem) {
IGenericClient client = provideClient();
Class<? extends IBaseBundle> bundleType = myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class);
IBaseBundle results = client
.search()
.forResource("CodeSystem")
.where(CodeSystem.URL.matches().value(theSystem))
.returnBundle(bundleType)
.execute();
List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results);
if (resultsList.size() > 0) {
return resultsList.get(0);
}
return null;
}
@Override
public IBaseResource fetchValueSet(String theValueSetUrl) {
IGenericClient client = provideClient();
Class<? extends IBaseBundle> bundleType = myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class);
IBaseBundle results = client
.search()
.forResource("ValueSet")
.where(CodeSystem.URL.matches().value(theValueSetUrl))
.returnBundle(bundleType)
.execute();
List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results);
if (resultsList.size() > 0) {
return resultsList.get(0);
}
return null;
}
@Override
public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
return fetchCodeSystem(theSystem) != null;
}
@Override
public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) {
return fetchValueSet(theValueSetUrl) != null;
}
private IGenericClient provideClient() {
IGenericClient retVal = myCtx.newRestfulGenericClient(myBaseUrl);
for (Object next : myClientInterceptors) {
@ -50,11 +112,6 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
return retVal;
}
@Override
public CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, null, theValueSet);
}
protected CodeValidationResult invokeRemoteValidateCode(String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) {
if (isBlank(theCode)) {
return null;
@ -64,6 +121,18 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
IBaseParameters input = ParametersUtil.newInstance(getFhirContext());
String resourceType = "ValueSet";
if (theValueSet == null && theValueSetUrl == null) {
resourceType = "CodeSystem";
ParametersUtil.addParameterToParametersUri(getFhirContext(), input, "url", theCodeSystem);
ParametersUtil.addParameterToParametersString(getFhirContext(), input, "code", theCode);
if (isNotBlank(theDisplay)) {
ParametersUtil.addParameterToParametersString(getFhirContext(), input, "display", theDisplay);
}
} else {
if (isNotBlank(theValueSetUrl)) {
ParametersUtil.addParameterToParametersUri(getFhirContext(), input, "url", theValueSetUrl);
}
@ -78,9 +147,12 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
ParametersUtil.addParameterToParameters(getFhirContext(), input, "valueSet", theValueSet);
}
}
IBaseParameters output = client
.operation()
.onType("ValueSet")
.onType(resourceType)
.named("validate-code")
.withParameters(input)
.execute();

View File

@ -5,9 +5,15 @@ import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.test.utilities.server.RestfulServerRule;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Parameters;
@ -21,6 +27,9 @@ import org.junit.Test;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
@ -37,13 +46,18 @@ public class RemoteTerminologyServiceValidationSupportTest {
@Rule
public RestfulServerRule myRestfulServerRule = new RestfulServerRule(ourCtx);
private MyMockTerminologyServiceProvider myProvider;
private MyValueSetProvider myValueSetProvider;
private RemoteTerminologyServiceValidationSupport mySvc;
private MyCodeSystemProvider myCodeSystemProvider;
@Before
public void before() {
myProvider = new MyMockTerminologyServiceProvider();
myRestfulServerRule.getRestfulServer().registerProvider(myProvider);
myValueSetProvider = new MyValueSetProvider();
myRestfulServerRule.getRestfulServer().registerProvider(myValueSetProvider);
myCodeSystemProvider = new MyCodeSystemProvider();
myRestfulServerRule.getRestfulServer().registerProvider(myCodeSystemProvider);
String baseUrl = "http://localhost:" + myRestfulServerRule.getPort();
mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx);
@ -53,7 +67,7 @@ public class RemoteTerminologyServiceValidationSupportTest {
@After
public void after() {
assertThat(myProvider.myInvocationCount, lessThan(2));
assertThat(myValueSetProvider.myInvocationCount, lessThan(2));
}
@Test
@ -64,7 +78,7 @@ public class RemoteTerminologyServiceValidationSupportTest {
@Test
public void testValidateCode_SystemCodeDisplayUrl_Success() {
createNextReturnParameters(true, DISPLAY, null);
createNextValueSetReturnParameters(true, DISPLAY, null);
IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(null, null, CODE_SYSTEM, CODE, DISPLAY, VALUE_SET_URL);
assertEquals(CODE, outcome.getCode());
@ -72,16 +86,16 @@ public class RemoteTerminologyServiceValidationSupportTest {
assertEquals(null, outcome.getSeverity());
assertEquals(null, outcome.getMessage());
assertEquals(CODE, myProvider.myLastCode.getCode());
assertEquals(DISPLAY, myProvider.myLastDisplay.getValue());
assertEquals(CODE_SYSTEM, myProvider.myLastSystem.getValue());
assertEquals(VALUE_SET_URL, myProvider.myLastUrl.getValue());
assertEquals(null, myProvider.myLastValueSet);
assertEquals(CODE, myValueSetProvider.myLastCode.getCode());
assertEquals(DISPLAY, myValueSetProvider.myLastDisplay.getValue());
assertEquals(CODE_SYSTEM, myValueSetProvider.myLastSystem.getValue());
assertEquals(VALUE_SET_URL, myValueSetProvider.myLastUrl.getValue());
assertEquals(null, myValueSetProvider.myLastValueSet);
}
@Test
public void testValidateCode_SystemCodeDisplayUrl_Error() {
createNextReturnParameters(false, null, ERROR_MESSAGE);
createNextValueSetReturnParameters(false, null, ERROR_MESSAGE);
IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(null, null, CODE_SYSTEM, CODE, DISPLAY, VALUE_SET_URL);
assertEquals(null, outcome.getCode());
@ -89,16 +103,32 @@ public class RemoteTerminologyServiceValidationSupportTest {
assertEquals(IValidationSupport.IssueSeverity.ERROR, outcome.getSeverity());
assertEquals(ERROR_MESSAGE, outcome.getMessage());
assertEquals(CODE, myProvider.myLastCode.getCode());
assertEquals(DISPLAY, myProvider.myLastDisplay.getValue());
assertEquals(CODE_SYSTEM, myProvider.myLastSystem.getValue());
assertEquals(VALUE_SET_URL, myProvider.myLastUrl.getValue());
assertEquals(null, myProvider.myLastValueSet);
assertEquals(CODE, myValueSetProvider.myLastCode.getCode());
assertEquals(DISPLAY, myValueSetProvider.myLastDisplay.getValue());
assertEquals(CODE_SYSTEM, myValueSetProvider.myLastSystem.getValue());
assertEquals(VALUE_SET_URL, myValueSetProvider.myLastUrl.getValue());
assertEquals(null, myValueSetProvider.myLastValueSet);
}
@Test
public void testValidateCodeInCodeSystem_Good() {
createNextCodeSystemReturnParameters(true, DISPLAY, null);
IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(null, null, CODE_SYSTEM, CODE, DISPLAY, null);
assertEquals(CODE, outcome.getCode());
assertEquals(DISPLAY, outcome.getDisplay());
assertEquals(null, outcome.getSeverity());
assertEquals(null, outcome.getMessage());
assertEquals(CODE, myCodeSystemProvider.myLastCode.getCode());
assertEquals(DISPLAY, myCodeSystemProvider.myLastDisplay.getValue());
assertEquals(CODE_SYSTEM, myCodeSystemProvider.myLastUrl.getValueAsString());
}
@Test
public void testValidateCodeInValueSet_SystemCodeDisplayVS_Good() {
createNextReturnParameters(true, DISPLAY, null);
createNextValueSetReturnParameters(true, DISPLAY, null);
ValueSet valueSet = new ValueSet();
valueSet.setUrl(VALUE_SET_URL);
@ -109,34 +139,127 @@ public class RemoteTerminologyServiceValidationSupportTest {
assertEquals(null, outcome.getSeverity());
assertEquals(null, outcome.getMessage());
assertEquals(CODE, myProvider.myLastCode.getCode());
assertEquals(DISPLAY, myProvider.myLastDisplay.getValue());
assertEquals(CODE_SYSTEM, myProvider.myLastSystem.getValue());
assertEquals(null, myProvider.myLastUrl);
assertEquals(VALUE_SET_URL, myProvider.myLastValueSet.getUrl());
assertEquals(CODE, myValueSetProvider.myLastCode.getCode());
assertEquals(DISPLAY, myValueSetProvider.myLastDisplay.getValue());
assertEquals(CODE_SYSTEM, myValueSetProvider.myLastSystem.getValue());
assertEquals(VALUE_SET_URL, myValueSetProvider.myLastUrl.getValueAsString());
assertEquals(null, myValueSetProvider.myLastValueSet);
}
public void createNextReturnParameters(boolean theResult, String theDisplay, String theMessage) {
myProvider.myNextReturn = new Parameters();
myProvider.myNextReturn.addParameter("result", theResult);
myProvider.myNextReturn.addParameter("display", theDisplay);
@Test
public void testIsValueSetSupported_False() {
myValueSetProvider.myNextReturnValueSets = new ArrayList<>();
boolean outcome = mySvc.isValueSetSupported(null, "http://loinc.org/VS");
assertEquals(false, outcome);
assertEquals("http://loinc.org/VS", myValueSetProvider.myLastUrlParam.getValue());
}
@Test
public void testIsValueSetSupported_True() {
myValueSetProvider.myNextReturnValueSets = new ArrayList<>();
myValueSetProvider.myNextReturnValueSets.add((ValueSet) new ValueSet().setId("ValueSet/123"));
boolean outcome = mySvc.isValueSetSupported(null, "http://loinc.org/VS");
assertEquals(true, outcome);
assertEquals("http://loinc.org/VS", myValueSetProvider.myLastUrlParam.getValue());
}
@Test
public void testIsCodeSystemSupported_False() {
myCodeSystemProvider.myNextReturnCodeSystems = new ArrayList<>();
boolean outcome = mySvc.isCodeSystemSupported(null, "http://loinc.org");
assertEquals(false, outcome);
assertEquals("http://loinc.org", myCodeSystemProvider.myLastUrlParam.getValue());
}
@Test
public void testIsCodeSystemSupported_True() {
myCodeSystemProvider.myNextReturnCodeSystems = new ArrayList<>();
myCodeSystemProvider.myNextReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/123"));
boolean outcome = mySvc.isCodeSystemSupported(null, "http://loinc.org");
assertEquals(true, outcome);
assertEquals("http://loinc.org", myCodeSystemProvider.myLastUrlParam.getValue());
}
private void createNextCodeSystemReturnParameters(boolean theResult, String theDisplay, String theMessage) {
myCodeSystemProvider.myNextReturnParams = new Parameters();
myCodeSystemProvider.myNextReturnParams.addParameter("result", theResult);
myCodeSystemProvider.myNextReturnParams.addParameter("display", theDisplay);
if (theMessage != null) {
myProvider.myNextReturn.addParameter("message", theMessage);
myCodeSystemProvider.myNextReturnParams.addParameter("message", theMessage);
}
}
private static class MyMockTerminologyServiceProvider {
private void createNextValueSetReturnParameters(boolean theResult, String theDisplay, String theMessage) {
myValueSetProvider.myNextReturnParams = new Parameters();
myValueSetProvider.myNextReturnParams.addParameter("result", theResult);
myValueSetProvider.myNextReturnParams.addParameter("display", theDisplay);
if (theMessage != null) {
myValueSetProvider.myNextReturnParams.addParameter("message", theMessage);
}
}
private static class MyCodeSystemProvider implements IResourceProvider {
private UriParam myLastUrlParam;
private List<CodeSystem> myNextReturnCodeSystems;
private int myInvocationCount;
private UriType myLastUrl;
private CodeType myLastCode;
private StringType myLastDisplay;
private Parameters myNextReturnParams;
@Operation(name = "validate-code", idempotent = true, returnParameters = {
@OperationParam(name = "result", type = BooleanType.class, min = 1),
@OperationParam(name = "message", type = StringType.class),
@OperationParam(name = "display", type = StringType.class)
})
public Parameters validateCode(
HttpServletRequest theServletRequest,
@IdParam(optional = true) IdType theId,
@OperationParam(name = "url", min = 0, max = 1) UriType theCodeSystemUrl,
@OperationParam(name = "code", min = 0, max = 1) CodeType theCode,
@OperationParam(name = "display", min = 0, max = 1) StringType theDisplay
) {
myInvocationCount++;
myLastUrl = theCodeSystemUrl;
myLastCode = theCode;
myLastDisplay = theDisplay;
return myNextReturnParams;
}
@Search
public List<CodeSystem> find(@RequiredParam(name="url") UriParam theUrlParam) {
myLastUrlParam = theUrlParam;
assert myNextReturnCodeSystems != null;
return myNextReturnCodeSystems;
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return CodeSystem.class;
}
}
private Parameters myNextReturn;
private static class MyValueSetProvider implements IResourceProvider {
private Parameters myNextReturnParams;
private List<ValueSet> myNextReturnValueSets;
private UriType myLastUrl;
private CodeType myLastCode;
private int myInvocationCount;
private UriType myLastSystem;
private StringType myLastDisplay;
private ValueSet myLastValueSet;
private UriParam myLastUrlParam;
@Operation(name = "validate-code", idempotent = true, typeName = "ValueSet", returnParameters = {
@Operation(name = "validate-code", idempotent = true, returnParameters = {
@OperationParam(name = "result", type = BooleanType.class, min = 1),
@OperationParam(name = "message", type = StringType.class),
@OperationParam(name = "display", type = StringType.class)
@ -156,11 +279,22 @@ public class RemoteTerminologyServiceValidationSupportTest {
myLastSystem = theSystem;
myLastDisplay = theDisplay;
myLastValueSet = theValueSet;
return myNextReturn;
return myNextReturnParams;
}
@Search
public List<ValueSet> find(@RequiredParam(name="url") UriParam theUrlParam) {
myLastUrlParam = theUrlParam;
assert myNextReturnValueSets != null;
return myNextReturnValueSets;
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return ValueSet.class;
}
}
}
}