diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java index 3624d4146b8..d0990842fa2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java @@ -1071,8 +1071,9 @@ public interface IValidationSupport { } } - public void setErrorMessage(String theErrorMessage) { + public LookupCodeResult setErrorMessage(String theErrorMessage) { myErrorMessage = theErrorMessage; + return this; } public String getErrorMessage() { diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index 686f1452d6d..129a88a40e8 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -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. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6150-validate-returns-404-valueset-with-remote-terminology-systems.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6150-validate-returns-404-valueset-with-remote-terminology-systems.yaml new file mode 100644 index 00000000000..ee2b7883248 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6150-validate-returns-404-valueset-with-remote-terminology-systems.yaml @@ -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." diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java index a75e7c73406..5f9705a8a50 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java @@ -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 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 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)) { diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java index e0d693d1ea4..0b20821af34 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java @@ -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); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyLookupCodeTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyLookupCodeTest.java new file mode 100644 index 00000000000..156ecea62fa --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyLookupCodeTest.java @@ -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); + } +} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeDstu3Test.java index 6269733654e..7bdf0c42518 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeDstu3Test.java @@ -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); } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java index ec6c018bb5f..3fc83b2042a 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java @@ -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