validate code on remote terminology service returns nullpointerexception (#4958)

* validate-code on remote-terminology service returns NullPointerException - tests

* validate-code on remote-terminology service returns NullPointerException - fixed

* validate-code on remote-terminology service returns NullPointerException - added changelog

* validate-code on remote-terminology service returns NullPointerException - tests readability improvement and fixes

* validate-code on remote-terminology service returns NullPointerException - fixes
This commit is contained in:
volodymyr-korzh 2023-06-08 10:47:39 -06:00 committed by GitHub
parent fddea8db92
commit 737238b97d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 122 additions and 16 deletions

View File

@ -0,0 +1,4 @@
---
type: fix
issue: 4957
title: "Previously, performing a $validate-code operation with a remote terminology service on an invalid CodeSystem or ValueSet returned 500. This problem has been fixed."

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.ConceptValidationOptions;
import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationResult;
import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem;
@ -41,6 +42,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import static ca.uhn.fhir.jpa.provider.ValueSetOperationProvider.toValidateCodeResult; import static ca.uhn.fhir.jpa.provider.ValueSetOperationProvider.toValidateCodeResult;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -141,7 +144,7 @@ public abstract class BaseJpaResourceProviderCodeSystem<T extends IBaseResource>
RequestDetails theRequestDetails RequestDetails theRequestDetails
) { ) {
IValidationSupport.CodeValidationResult result = null; CodeValidationResult result = null;
startRequest(theServletRequest); startRequest(theServletRequest);
try { try {
// TODO: JA why not just always just the chain here? and we can then get rid of the corresponding DAO method entirely // TODO: JA why not just always just the chain here? and we can then get rid of the corresponding DAO method entirely
@ -159,9 +162,8 @@ public abstract class BaseJpaResourceProviderCodeSystem<T extends IBaseResource>
String code = theCoding.getCode(); String code = theCoding.getCode();
String display = theCoding.getDisplay(); String display = theCoding.getDisplay();
result = myValidationSupportChain.validateCode( result = validateCodeWithTerminologyService(codeSystemUrl, code, display)
new ValidationSupportContext(myValidationSupportChain), new ConceptValidationOptions(), .orElseGet(supplyUnableToValidateResult(codeSystemUrl, code));
codeSystemUrl, code, display, null);
} }
} }
} else { } else {
@ -175,4 +177,13 @@ public abstract class BaseJpaResourceProviderCodeSystem<T extends IBaseResource>
} }
} }
private Optional<CodeValidationResult> validateCodeWithTerminologyService(String theCodeSystemUrl, String theCode, String theDisplay) {
return Optional.ofNullable(myValidationSupportChain.validateCode(new ValidationSupportContext(myValidationSupportChain),
new ConceptValidationOptions(), theCodeSystemUrl, theCode, theDisplay, null));
}
private Supplier<CodeValidationResult> supplyUnableToValidateResult(String theCodeSystemUrl, String theCode) {
return () -> new CodeValidationResult().setMessage("Terminology service was unable to provide validation for " + theCodeSystemUrl + "#" + theCode);
}
} }

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.ConceptValidationOptions;
import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationResult;
import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.context.support.ValueSetExpansionOptions; import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.i18n.Msg;
@ -53,6 +54,9 @@ import org.springframework.beans.factory.annotation.Qualifier;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.Optional;
import java.util.function.Supplier;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class ValueSetOperationProvider extends BaseJpaProvider { public class ValueSetOperationProvider extends BaseJpaProvider {
@ -128,7 +132,7 @@ public class ValueSetOperationProvider extends BaseJpaProvider {
RequestDetails theRequestDetails RequestDetails theRequestDetails
) { ) {
IValidationSupport.CodeValidationResult result; CodeValidationResult result;
startRequest(theServletRequest); startRequest(theServletRequest);
try { try {
// If a Remote Terminology Server has been configured, use it // If a Remote Terminology Server has been configured, use it
@ -138,8 +142,20 @@ public class ValueSetOperationProvider extends BaseJpaProvider {
String theDisplayString = (theDisplay != null && theDisplay.hasValue()) ? theDisplay.getValueAsString() : null; String theDisplayString = (theDisplay != null && theDisplay.hasValue()) ? theDisplay.getValueAsString() : null;
String theValueSetUrlString = (theValueSetUrl != null && theValueSetUrl.hasValue()) ? String theValueSetUrlString = (theValueSetUrl != null && theValueSetUrl.hasValue()) ?
theValueSetUrl.getValueAsString() : null; theValueSetUrl.getValueAsString() : null;
result = myValidationSupportChain.validateCode(new ValidationSupportContext(myValidationSupportChain), if (theCoding != null) {
new ConceptValidationOptions(), theSystemString, theCodeString, theDisplayString, theValueSetUrlString); if (isNotBlank(theCoding.getSystem())) {
if (theSystemString != null && !theSystemString.equalsIgnoreCase(theCoding.getSystem())) {
throw new InvalidRequestException(Msg.code(2352) + "Coding.system '" + theCoding.getSystem() +
"' does not equal param system '" + theSystemString + "'. Unable to validate-code.");
}
theSystemString = theCoding.getSystem();
theCodeString = theCoding.getCode();
theDisplayString = theCoding.getDisplay();
}
}
result = validateCodeWithTerminologyService(theSystemString, theCodeString, theDisplayString, theValueSetUrlString)
.orElseGet(supplyUnableToValidateResult(theSystemString, theCodeString, theValueSetUrlString));
} else { } else {
// Otherwise, use the local DAO layer to validate the code // Otherwise, use the local DAO layer to validate the code
IFhirResourceDaoValueSet<IBaseResource> dao = getDao(); IFhirResourceDaoValueSet<IBaseResource> dao = getDao();
@ -165,6 +181,17 @@ public class ValueSetOperationProvider extends BaseJpaProvider {
} }
} }
private Optional<CodeValidationResult> validateCodeWithTerminologyService(String theSystem, String theCode,
String theDisplay, String theValueSetUrl) {
return Optional.ofNullable(myValidationSupportChain.validateCode(new ValidationSupportContext(myValidationSupportChain),
new ConceptValidationOptions(), theSystem, theCode, theDisplay, theValueSetUrl));
}
private Supplier<CodeValidationResult> supplyUnableToValidateResult(String theSystem, String theCode, String theValueSetUrl) {
return () -> new CodeValidationResult().setMessage("Validator is unable to provide validation for " +
theCode + "#" + theSystem + " - Unknown or unusable ValueSet[" + theValueSetUrl + "]");
}
@Operation(name = ProviderConstants.OPERATION_INVALIDATE_EXPANSION, idempotent = false, typeName = "ValueSet", returnParameters = { @Operation(name = ProviderConstants.OPERATION_INVALIDATE_EXPANSION, idempotent = false, typeName = "ValueSet", returnParameters = {
@OperationParam(name = "message", typeName = "string", min = 1, max = 1) @OperationParam(name = "message", typeName = "string", min = 1, max = 1)
}) })
@ -228,7 +255,7 @@ public class ValueSetOperationProvider extends BaseJpaProvider {
return options; return options;
} }
public static IBaseParameters toValidateCodeResult(FhirContext theContext, IValidationSupport.CodeValidationResult theResult) { public static IBaseParameters toValidateCodeResult(FhirContext theContext, CodeValidationResult theResult) {
IBaseParameters retVal = ParametersUtil.newInstance(theContext); IBaseParameters retVal = ParametersUtil.newInstance(theContext);
ParametersUtil.addParameterToParametersBoolean(theContext, retVal, "result", theResult.isOk()); ParametersUtil.addParameterToParametersBoolean(theContext, retVal, "result", theResult.isOk());

View File

@ -37,7 +37,9 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
/* /*
* This set of Unit Tests instantiates and injects an instance of * This set of Unit Tests instantiates and injects an instance of
@ -51,6 +53,9 @@ public class ResourceProviderR4RemoteTerminologyTest extends BaseResourceProvide
private static final String DISPLAY = "DISPLAY"; private static final String DISPLAY = "DISPLAY";
private static final String DISPLAY_BODY_MASS_INDEX = "Body mass index (BMI) [Ratio]"; private static final String DISPLAY_BODY_MASS_INDEX = "Body mass index (BMI) [Ratio]";
private static final String CODE_BODY_MASS_INDEX = "39156-5"; private static final String CODE_BODY_MASS_INDEX = "39156-5";
private static final String CODE_SYSTEM_V2_0247_URI = "http://terminology.hl7.org/CodeSystem/v2-0247";
private static final String INVALID_CODE_SYSTEM_URI = "http://terminology.hl7.org/CodeSystem/INVALID-CODESYSTEM";
private static final String UNKNOWN_VALUE_SYSTEM_URI = "http://hl7.org/fhir/ValueSet/unknown-value-set";
private static FhirContext ourCtx = FhirContext.forR4(); private static FhirContext ourCtx = FhirContext.forR4();
private MyCodeSystemProvider myCodeSystemProvider = new MyCodeSystemProvider(); private MyCodeSystemProvider myCodeSystemProvider = new MyCodeSystemProvider();
private MyValueSetProvider myValueSetProvider = new MyValueSetProvider(); private MyValueSetProvider myValueSetProvider = new MyValueSetProvider();
@ -79,20 +84,20 @@ public class ResourceProviderR4RemoteTerminologyTest extends BaseResourceProvide
} }
@Test @Test
public void testValidateCodeOperationOnCodeSystem_ByCodingAndUrlWhereSystemIsDifferent_ThrowsException() { public void testValidateCodeOperationOnCodeSystem_byCodingAndUrlWhereSystemIsDifferent_throwsException() {
assertThrows(InvalidRequestException.class, () -> { assertThrows(InvalidRequestException.class, () -> {
Parameters respParam = myClient Parameters respParam = myClient
.operation() .operation()
.onType(CodeSystem.class) .onType(CodeSystem.class)
.named(JpaConstants.OPERATION_VALIDATE_CODE) .named(JpaConstants.OPERATION_VALIDATE_CODE)
.withParameter(Parameters.class, "coding", new Coding().setSystem("http://terminology.hl7.org/CodeSystem/v2-0247").setCode("P")) .withParameter(Parameters.class, "coding", new Coding().setSystem(CODE_SYSTEM_V2_0247_URI).setCode("P"))
.andParameter("url", new UriType("http://terminology.hl7.org/CodeSystem/INVALID-CODESYSTEM")) .andParameter("url", new UriType(INVALID_CODE_SYSTEM_URI))
.execute(); .execute();
}); });
} }
@Test @Test
public void testValidateCodeOperationOnCodeSystem_ByCodingAndUrl_UsingBuiltInCodeSystems() { public void testValidateCodeOperationOnCodeSystem_byCodingAndUrl_usingBuiltInCodeSystems() {
myCodeSystemProvider.myNextReturnCodeSystems = new ArrayList<>(); myCodeSystemProvider.myNextReturnCodeSystems = new ArrayList<>();
myCodeSystemProvider.myNextReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/v2-0247")); myCodeSystemProvider.myNextReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/v2-0247"));
createNextCodeSystemReturnParameters(true, DISPLAY, null); createNextCodeSystemReturnParameters(true, DISPLAY, null);
@ -103,8 +108,8 @@ public class ResourceProviderR4RemoteTerminologyTest extends BaseResourceProvide
.operation() .operation()
.onType(CodeSystem.class) .onType(CodeSystem.class)
.named(JpaConstants.OPERATION_VALIDATE_CODE) .named(JpaConstants.OPERATION_VALIDATE_CODE)
.withParameter(Parameters.class, "coding", new Coding().setSystem("http://terminology.hl7.org/CodeSystem/v2-0247").setCode("P")) .withParameter(Parameters.class, "coding", new Coding().setSystem(CODE_SYSTEM_V2_0247_URI).setCode("P"))
.andParameter("url", new UriType("http://terminology.hl7.org/CodeSystem/v2-0247")) .andParameter("url", new UriType(CODE_SYSTEM_V2_0247_URI))
.execute(); .execute();
String resp = myFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam); String resp = myFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam);
@ -115,7 +120,45 @@ public class ResourceProviderR4RemoteTerminologyTest extends BaseResourceProvide
} }
@Test @Test
public void testValidateCodeOperationOnValueSet_ByUrlAndSystem_UsingBuiltInCodeSystems() { public void testValidateCodeOperationOnCodeSystem_byCodingAndUrlWhereCodeSystemIsUnknown_returnsFalse() {
myCodeSystemProvider.myNextReturnCodeSystems = new ArrayList<>();
Parameters respParam = myClient
.operation()
.onType(CodeSystem.class)
.named(JpaConstants.OPERATION_VALIDATE_CODE)
.withParameter(Parameters.class, "coding", new Coding()
.setSystem(INVALID_CODE_SYSTEM_URI).setCode("P"))
.andParameter("url", new UriType(INVALID_CODE_SYSTEM_URI))
.execute();
String resp = myFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam);
ourLog.info(resp);
assertFalse(((BooleanType) respParam.getParameterValue("result")).booleanValue());
assertEquals("Terminology service was unable to provide validation for " + INVALID_CODE_SYSTEM_URI +
"#P", respParam.getParameterValue("message").toString());
}
@Test
public void testValidateCodeOperationOnValueSet_byCodingAndUrlWhereSystemIsDifferent_throwsException() {
try {
myClient.operation()
.onType(ValueSet.class)
.named(JpaConstants.OPERATION_VALIDATE_CODE)
.withParameter(Parameters.class, "coding", new Coding().setSystem(CODE_SYSTEM_V2_0247_URI).setCode("P"))
.andParameter("url", new UriType("http://hl7.org/fhir/ValueSet/list-example-codes"))
.andParameter("system", new UriType(INVALID_CODE_SYSTEM_URI))
.execute();
fail();
} catch (InvalidRequestException exception) {
assertEquals("HTTP 400 Bad Request: HAPI-2352: Coding.system '" + CODE_SYSTEM_V2_0247_URI + "' " +
"does not equal param system '" + INVALID_CODE_SYSTEM_URI + "'. Unable to validate-code.", exception.getMessage());
}
}
@Test
public void testValidateCodeOperationOnValueSet_byUrlAndSystem_usingBuiltInCodeSystems() {
myCodeSystemProvider.myNextReturnCodeSystems = new ArrayList<>(); myCodeSystemProvider.myNextReturnCodeSystems = new ArrayList<>();
myCodeSystemProvider.myNextReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/list-example-use-codes")); myCodeSystemProvider.myNextReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/list-example-use-codes"));
myValueSetProvider.myNextReturnValueSets = new ArrayList<>(); myValueSetProvider.myNextReturnValueSets = new ArrayList<>();
@ -140,7 +183,7 @@ public class ResourceProviderR4RemoteTerminologyTest extends BaseResourceProvide
} }
@Test @Test
public void testValidateCodeOperationOnValueSet_ByUrlSystemAndCode() { public void testValidateCodeOperationOnValueSet_byUrlSystemAndCode() {
myCodeSystemProvider.myNextReturnCodeSystems = new ArrayList<>(); myCodeSystemProvider.myNextReturnCodeSystems = new ArrayList<>();
myCodeSystemProvider.myNextReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/list-example-use-codes")); myCodeSystemProvider.myNextReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/list-example-use-codes"));
myValueSetProvider.myNextReturnValueSets = new ArrayList<>(); myValueSetProvider.myNextReturnValueSets = new ArrayList<>();
@ -163,6 +206,27 @@ public class ResourceProviderR4RemoteTerminologyTest extends BaseResourceProvide
assertEquals(DISPLAY_BODY_MASS_INDEX, respParam.getParameterValue("display").toString()); assertEquals(DISPLAY_BODY_MASS_INDEX, respParam.getParameterValue("display").toString());
} }
@Test
public void testValidateCodeOperationOnValueSet_byCodingAndUrlWhereValueSetIsUnknown_returnsFalse() {
myValueSetProvider.myNextReturnValueSets = new ArrayList<>();
Parameters respParam = myClient
.operation()
.onType(ValueSet.class)
.named(JpaConstants.OPERATION_VALIDATE_CODE)
.withParameter(Parameters.class, "coding", new Coding()
.setSystem(CODE_SYSTEM_V2_0247_URI).setCode("P"))
.andParameter("url", new UriType(UNKNOWN_VALUE_SYSTEM_URI))
.execute();
String resp = myFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam);
ourLog.info(resp);
assertFalse(((BooleanType) respParam.getParameterValue("result")).booleanValue());
assertEquals("Validator is unable to provide validation for P#" + CODE_SYSTEM_V2_0247_URI +
" - Unknown or unusable ValueSet[" + UNKNOWN_VALUE_SYSTEM_URI + "]", respParam.getParameterValue("message").toString());
}
private void createNextCodeSystemReturnParameters(boolean theResult, String theDisplay, String theMessage) { private void createNextCodeSystemReturnParameters(boolean theResult, String theDisplay, String theMessage) {
myCodeSystemProvider.myNextReturnParams = new Parameters(); myCodeSystemProvider.myNextReturnParams = new Parameters();
myCodeSystemProvider.myNextReturnParams.addParameter("result", theResult); myCodeSystemProvider.myNextReturnParams.addParameter("result", theResult);