Handle 400 and 404 codes returned by remote terminology operation. (#6151)

* Handle 400 and 404 codes returned by remote terminology  operation.

* Some simplification

* Adjust changelog

* Add a comment to explain alternate solution which can be reused.
This commit is contained in:
Martha Mitran 2024-07-26 10:10:44 -07:00 committed by GitHub
parent 13ae273cbf
commit 10eaad3cbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 160 additions and 46 deletions

View File

@ -1071,8 +1071,9 @@ public interface IValidationSupport {
}
}
public void setErrorMessage(String theErrorMessage) {
public LookupCodeResult setErrorMessage(String theErrorMessage) {
myErrorMessage = theErrorMessage;
return this;
}
public String getErrorMessage() {

View File

@ -6,6 +6,8 @@ org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.
org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.mismatchCodeSystem=Inappropriate CodeSystem URL "{0}" for ValueSet: {1}
org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.codeNotFoundInValueSet=Code "{0}" is not in valueset: {1}
org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.unknownCodeInSystem=Unknown code "{0}#{1}". The Remote Terminology server {2} returned {3}
ca.uhn.fhir.jpa.term.TermReadSvcImpl.expansionRefersToUnknownCs=Unknown CodeSystem URI "{0}" referenced from ValueSet
ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetNotYetExpanded=ValueSet "{0}" has not yet been pre-expanded. Performing in-memory expansion without parameters. Current status: {1} | {2}
ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetNotYetExpanded_OffsetNotAllowed=ValueSet expansion can not combine "offset" with "ValueSet.compose.exclude" unless the ValueSet has been pre-expanded. ValueSet "{0}" must be pre-expanded for this operation to work.

View File

@ -0,0 +1,6 @@
---
type: fix
issue: 6150
title: "Previously, the resource $validate operation would return a 404 when the associated profile uses a ValueSet
that has multiple includes referencing Remote Terminology CodeSystem resources.
This has been fixed to return a 200 with issues instead."

View File

@ -12,6 +12,8 @@ import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.rest.api.SummaryEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.gclient.IQuery;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.ParametersUtil;
import jakarta.annotation.Nonnull;
@ -204,43 +206,56 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
FhirContext fhirContext = client.getFhirContext();
FhirVersionEnum fhirVersion = fhirContext.getVersion().getVersion();
switch (fhirVersion) {
case DSTU3:
case R4:
IBaseParameters params = ParametersUtil.newInstance(fhirContext);
ParametersUtil.addParameterToParametersString(fhirContext, params, "code", code);
if (!StringUtils.isEmpty(system)) {
ParametersUtil.addParameterToParametersString(fhirContext, params, "system", system);
}
if (!StringUtils.isEmpty(displayLanguage)) {
ParametersUtil.addParameterToParametersString(fhirContext, params, "language", displayLanguage);
}
for (String propertyName : theLookupCodeRequest.getPropertyNames()) {
ParametersUtil.addParameterToParametersCode(fhirContext, params, "property", propertyName);
}
Class<? extends IBaseResource> codeSystemClass =
myCtx.getResourceDefinition("CodeSystem").getImplementingClass();
IBaseParameters outcome = client.operation()
.onType(codeSystemClass)
.named("$lookup")
.withParameters(params)
.useHttpGet()
.execute();
if (outcome != null && !outcome.isEmpty()) {
switch (fhirVersion) {
case DSTU3:
return generateLookupCodeResultDstu3(
code, system, (org.hl7.fhir.dstu3.model.Parameters) outcome);
case R4:
return generateLookupCodeResultR4(code, system, (Parameters) outcome);
}
}
break;
default:
throw new UnsupportedOperationException(Msg.code(710) + "Unsupported FHIR version '"
+ fhirVersion.getFhirVersionString() + "'. Only DSTU3 and R4 are supported.");
if (fhirVersion.isNewerThan(FhirVersionEnum.R4) || fhirVersion.isOlderThan(FhirVersionEnum.DSTU3)) {
throw new UnsupportedOperationException(Msg.code(710) + "Unsupported FHIR version '"
+ fhirVersion.getFhirVersionString() + "'. Only DSTU3 and R4 are supported.");
}
return null;
IBaseParameters params = ParametersUtil.newInstance(fhirContext);
ParametersUtil.addParameterToParametersString(fhirContext, params, "code", code);
if (!StringUtils.isEmpty(system)) {
ParametersUtil.addParameterToParametersString(fhirContext, params, "system", system);
}
if (!StringUtils.isEmpty(displayLanguage)) {
ParametersUtil.addParameterToParametersString(fhirContext, params, "language", displayLanguage);
}
for (String propertyName : theLookupCodeRequest.getPropertyNames()) {
ParametersUtil.addParameterToParametersCode(fhirContext, params, "property", propertyName);
}
Class<? extends IBaseResource> codeSystemClass =
myCtx.getResourceDefinition("CodeSystem").getImplementingClass();
IBaseParameters outcome;
try {
outcome = client.operation()
.onType(codeSystemClass)
.named("$lookup")
.withParameters(params)
.useHttpGet()
.execute();
} catch (ResourceNotFoundException | InvalidRequestException e) {
// this can potentially be moved to an interceptor and be reused in other areas
// where we call a remote server or by the client as a custom interceptor
// that interceptor would alter the status code of the response and the body into a different format
// e.g. ClientResponseInterceptorModificationTemplate
ourLog.error(e.getMessage(), e);
LookupCodeResult result = LookupCodeResult.notFound(system, code);
result.setErrorMessage(
getErrorMessage("unknownCodeInSystem", system, code, client.getServerBase(), e.getMessage()));
return result;
}
if (outcome != null && !outcome.isEmpty()) {
if (fhirVersion == FhirVersionEnum.DSTU3) {
return generateLookupCodeResultDstu3(code, system, (org.hl7.fhir.dstu3.model.Parameters) outcome);
}
if (fhirVersion == FhirVersionEnum.R4) {
return generateLookupCodeResultR4(code, system, (Parameters) outcome);
}
}
return LookupCodeResult.notFound(system, code);
}
protected String getErrorMessage(String errorCode, Object... theParams) {
return getFhirContext().getLocalizer().getMessage(getClass(), errorCode, theParams);
}
private LookupCodeResult generateLookupCodeResultDstu3(
@ -278,6 +293,7 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
case "abstract":
result.setCodeIsAbstract(Boolean.parseBoolean(parameterTypeAsString));
break;
default:
}
}
return result;
@ -384,6 +400,7 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
case "value":
conceptDesignation.setValue(designationComponent.getValue().toString());
break;
default:
}
}
return conceptDesignation;
@ -422,6 +439,7 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
case "abstract":
result.setCodeIsAbstract(Boolean.parseBoolean(parameterTypeAsString));
break;
default:
}
}
return result;
@ -508,6 +526,7 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
case "value":
conceptDesignation.setValue(designationComponentValue.toString());
break;
default:
}
}
return conceptDesignation;
@ -591,6 +610,10 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
return retVal;
}
public String getBaseUrl() {
return myBaseUrl;
}
protected CodeValidationResult invokeRemoteValidateCode(
String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) {
if (isBlank(theCode)) {

View File

@ -17,11 +17,11 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static ca.uhn.fhir.context.support.IValidationSupport.TYPE_CODING;
import static ca.uhn.fhir.context.support.IValidationSupport.TYPE_GROUP;
import static ca.uhn.fhir.context.support.IValidationSupport.TYPE_STRING;
import static java.util.stream.IntStream.range;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.createConceptProperty;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -54,7 +54,8 @@ public interface ILookupCodeTest {
default void lookupCode_forCodeSystemWithBlankCode_throwsException() {
try {
getService().lookupCode(null, new LookupCodeRequest(CODE_SYSTEM, ""));
fail(); } catch (IllegalArgumentException e) {
fail();
} catch (IllegalArgumentException e) {
assertEquals("theCode must be provided", e.getMessage());
}
}
@ -63,6 +64,7 @@ public interface ILookupCodeTest {
default void lookupCode_forCodeSystemWithPropertyInvalidType_throwsException() {
// test
LookupCodeResult result = new LookupCodeResult();
result.setFound(true);
result.getProperties().add(new BaseConceptProperty("someProperty") {
public String getType() {
return "someUnsupportedType";
@ -72,9 +74,10 @@ public interface ILookupCodeTest {
// test and verify
try {
getService().lookupCode(null, new LookupCodeRequest(CODE_SYSTEM, CODE, LANGUAGE, null));
fail(); } catch (InternalErrorException e) {
assertThat(e.getMessage()).contains("HAPI-1739: Don't know how to handle ");
getService().lookupCode(null, new LookupCodeRequest(CODE_SYSTEM, CODE, LANGUAGE, null));
fail();
} catch (InternalErrorException e) {
assertThat(e.getMessage()).contains("HAPI-1739: Don't know how to handle ");
}
}
@ -101,6 +104,7 @@ public interface ILookupCodeTest {
ConceptDesignation designation1 = new ConceptDesignation().setUseCode(code1).setUseSystem("system1").setValue("value1").setLanguage("en");
ConceptDesignation designation2 = new ConceptDesignation().setUseCode(code2).setUseSystem("system2").setValue("value2").setLanguage("es");
LookupCodeResult result = new LookupCodeResult();
result.setFound(true);
result.getDesignations().add(designation1);
result.getDesignations().add(designation2);
getCodeSystemProvider().setLookupCodeResult(result);
@ -184,6 +188,8 @@ public interface ILookupCodeTest {
assertNotNull(outcome);
assertEquals(theRequest.getCode(), getCodeSystemProvider().getCode());
assertEquals(theRequest.getSystem(), getCodeSystemProvider().getSystem());
assertEquals(theExpectedResult.isFound(), outcome.isFound());
assertEquals(theExpectedResult.getErrorMessage(), outcome.getErrorMessage());
assertEquals(theExpectedResult.getCodeSystemDisplayName(), outcome.getCodeSystemDisplayName());
assertEquals(theExpectedResult.getCodeDisplay(), outcome.getCodeDisplay());
assertEquals(theExpectedResult.getCodeSystemVersion(), outcome.getCodeSystemVersion());
@ -199,6 +205,7 @@ public interface ILookupCodeTest {
default void verifyLookupWithConceptDesignation(final ConceptDesignation theConceptDesignation) {
// setup
LookupCodeResult result = new LookupCodeResult();
result.setFound(true);
result.getDesignations().add(theConceptDesignation);
getCodeSystemProvider().setLookupCodeResult(result);

View File

@ -0,0 +1,57 @@
package org.hl7.fhir.common.hapi.validation;
import ca.uhn.fhir.context.support.IValidationSupport.LookupCodeResult;
import ca.uhn.fhir.context.support.LookupCodeRequest;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.junit.jupiter.api.Test;
import java.text.MessageFormat;
/**
* Additional tests specific for Remote Terminology $lookup operation.
* Please see base interface for additional tests, implementation agnostic.
*/
public interface IRemoteTerminologyLookupCodeTest extends ILookupCodeTest {
String MESSAGE_RESPONSE_NOT_FOUND = "Code {0} was not found";
String MESSAGE_RESPONSE_INVALID = "Code {0} lookup is missing a system";
@Override
RemoteTerminologyServiceValidationSupport getService();
@Test
default void lookupCode_forCodeSystemWithCodeNotFound_returnsNotFound() {
String baseUrl = getService().getBaseUrl();
final String codeNotFound = "a";
final String system = CODE_SYSTEM;
final String codeAndSystem = system + "#" + codeNotFound;
final String exceptionMessage = MessageFormat.format(MESSAGE_RESPONSE_NOT_FOUND, codeNotFound);
LookupCodeResult result = new LookupCodeResult()
.setFound(false)
.setSearchedForCode(codeNotFound)
.setSearchedForSystem(system)
.setErrorMessage("Unknown code \"" + codeAndSystem + "\". The Remote Terminology server " + baseUrl + " returned HTTP 404 Not Found: " + exceptionMessage);
getCodeSystemProvider().setLookupCodeResult(result);
LookupCodeRequest request = new LookupCodeRequest(system, codeNotFound, null, null);
verifyLookupCodeResult(request, result);
}
@Test
default void lookupCode_forCodeSystemWithInvalidRequest_returnsNotFound() {
String baseUrl = getService().getBaseUrl();
final String codeNotFound = "a";
final String system = null;
final String codeAndSystem = system + "#" + codeNotFound;
final String exceptionMessage = MessageFormat.format(MESSAGE_RESPONSE_INVALID, codeNotFound);
LookupCodeResult result = new LookupCodeResult()
.setFound(false)
.setSearchedForCode(codeNotFound)
.setSearchedForSystem(system)
.setErrorMessage("Unknown code \"" + codeAndSystem + "\". The Remote Terminology server " + baseUrl + " returned HTTP 400 Bad Request: " + exceptionMessage);
getCodeSystemProvider().setLookupCodeResult(result);
LookupCodeRequest request = new LookupCodeRequest(system, codeNotFound, null, null);
verifyLookupCodeResult(request, result);
}
}

View File

@ -9,9 +9,11 @@ import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import jakarta.servlet.http.HttpServletRequest;
import org.hl7.fhir.common.hapi.validation.ILookupCodeTest;
import org.hl7.fhir.common.hapi.validation.IRemoteTerminologyLookupCodeTest;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.hl7.fhir.dstu3.model.BooleanType;
import org.hl7.fhir.dstu3.model.CodeSystem;
@ -33,6 +35,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.text.MessageFormat;
import java.util.Calendar;
import java.util.List;
import java.util.stream.Stream;
@ -41,7 +44,7 @@ import java.util.stream.Stream;
* Version specific tests for CodeSystem $lookup against RemoteTerminologyValidationSupport.
* @see RemoteTerminologyServiceValidationSupport
*/
public class RemoteTerminologyLookupCodeDstu3Test implements ILookupCodeTest {
public class RemoteTerminologyLookupCodeDstu3Test implements IRemoteTerminologyLookupCodeTest {
private static final FhirContext ourCtx = FhirContext.forDstu3Cached();
@RegisterExtension
public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx);
@ -181,6 +184,12 @@ public class RemoteTerminologyLookupCodeDstu3Test implements ILookupCodeTest {
) {
myCode = theCode;
mySystemUrl = theSystem;
if (theSystem == null) {
throw new InvalidRequestException(MessageFormat.format(MESSAGE_RESPONSE_INVALID, theCode));
}
if (!myLookupCodeResult.isFound()) {
throw new ResourceNotFoundException(MessageFormat.format(MESSAGE_RESPONSE_NOT_FOUND, theCode));
}
return myLookupCodeResult.toParameters(theRequestDetails.getFhirContext(), thePropertyNames);
}

View File

@ -7,9 +7,11 @@ import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import jakarta.servlet.http.HttpServletRequest;
import org.hl7.fhir.common.hapi.validation.ILookupCodeTest;
import org.hl7.fhir.common.hapi.validation.IRemoteTerminologyLookupCodeTest;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseParameters;
@ -31,6 +33,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.text.MessageFormat;
import java.util.Calendar;
import java.util.List;
import java.util.stream.Stream;
@ -42,7 +45,7 @@ import static ca.uhn.fhir.context.support.IValidationSupport.LookupCodeResult;
* Version specific tests for CodeSystem $lookup against RemoteTerminologyValidationSupport.
* @see RemoteTerminologyServiceValidationSupport
*/
public class RemoteTerminologyLookupCodeR4Test implements ILookupCodeTest {
public class RemoteTerminologyLookupCodeR4Test implements IRemoteTerminologyLookupCodeTest {
private static final FhirContext ourCtx = FhirContext.forR4Cached();
@RegisterExtension
public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx);
@ -181,6 +184,12 @@ public class RemoteTerminologyLookupCodeR4Test implements ILookupCodeTest {
) {
myCode = theCode;
mySystemUrl = theSystem;
if (theSystem == null) {
throw new InvalidRequestException(MessageFormat.format(MESSAGE_RESPONSE_INVALID, theCode));
}
if (!myLookupCodeResult.isFound()) {
throw new ResourceNotFoundException(MessageFormat.format(MESSAGE_RESPONSE_NOT_FOUND, theCode));
}
return myLookupCodeResult.toParameters(theRequestDetails.getFhirContext(), thePropertyNames);
}
@Override