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 939c83dc0ceb..38bb69ad6e59 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 @@ -255,6 +255,22 @@ default boolean isCodeSystemSupported(ValidationSupportContext theValidationSupp return false; } + /** + * For most validation support modules, this method should return the same value as {@link IValidationSupport#isCodeSystemSupported(ValidationSupportContext, String)} and + * no specific implementation is required other than the default implementation provided here. + * A validation support module should override this only if it wants to generate a validation result even for an unsupported CodeSystem. + * For example, {@link org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValidationSupport} overrides this method to generate issues for unknown CodeSystems. + * + * @param theValidationSupportContext The validation support module will be passed in to this method. This is convenient in cases where the operation needs to make calls to + * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. + * @param theSystem The URI for the code system, e.g. "http://loinc.org" + * @return Returns true if a validation result can be generated by this validation support module even when the code system is not supported. + */ + default boolean canGenerateValidationResultForCodeSystem( + ValidationSupportContext theValidationSupportContext, String theSystem) { + return isCodeSystemSupported(theValidationSupportContext, theSystem); + } + /** * Returns true if a Remote Terminology Service is currently configured * diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index 47e8c06f7591..2d763443bb53 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -890,8 +890,11 @@ public ResourceLoaderImpl jpaResourceLoader() { @Bean public UnknownCodeSystemWarningValidationSupport unknownCodeSystemWarningValidationSupport( - FhirContext theFhirContext) { - return new UnknownCodeSystemWarningValidationSupport(theFhirContext); + FhirContext theFhirContext, JpaStorageSettings theStorageSettings) { + UnknownCodeSystemWarningValidationSupport support = + new UnknownCodeSystemWarningValidationSupport(theFhirContext); + support.setNonExistentCodeSystemSeverity(theStorageSettings.getIssueSeverityForUnknownCodeSystem()); + return support; } @Bean diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java index 17dca56bfbb1..ee9311a11a43 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java @@ -60,6 +60,7 @@ public InMemoryTerminologyServerValidationSupport inMemoryTerminologyServerValid InMemoryTerminologyServerValidationSupport retVal = new InMemoryTerminologyServerValidationSupport(theFhirContext); retVal.setIssueSeverityForCodeDisplayMismatch(theStorageSettings.getIssueSeverityForCodeDisplayMismatch()); + retVal.setIssueSeverityForUnknownCodeSystem(theStorageSettings.getIssueSeverityForUnknownCodeSystem()); return retVal; } diff --git a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3CodeSystemTest.java b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3CodeSystemTest.java index d63ca8723328..b68915809493 100644 --- a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3CodeSystemTest.java +++ b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3CodeSystemTest.java @@ -368,7 +368,7 @@ public void testValidateCodeOperation() { .map(t -> ((IPrimitiveType) t.getValue()).getValue()) .findFirst() .orElseThrow(IllegalArgumentException::new); - assertThat(message).contains("Terminology service was unable to provide validation for https://url#1"); + assertThat(message).contains("CodeSystem is unknown and can't be validated: https://url for 'https://url#1'"); } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index 8acae88b60eb..e72f5d3ee1ad 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -725,11 +725,11 @@ public void testValidate() { fail(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(e.getOperationOutcome())); } myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertEquals(11, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + assertEquals(12, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); assertEquals(0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); - assertEquals(9, myCaptureQueriesListener.countCommits()); + assertEquals(10, myCaptureQueriesListener.countCommits()); // Validate again (should rely only on caches) myCaptureQueriesListener.clear(); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java index cefea35770c3..f7c76124a31e 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java @@ -60,6 +60,7 @@ import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.IntegerType; import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.Medication; import org.hl7.fhir.r4.model.Narrative; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; @@ -155,9 +156,11 @@ public void after() { myValidationSettings.setLocalReferenceValidationDefaultPolicy(ReferenceValidationPolicy.IGNORE); myFhirContext.setParserErrorHandler(new StrictErrorHandler()); - myUnknownCodeSystemWarningValidationSupport.setNonExistentCodeSystemSeverity(UnknownCodeSystemWarningValidationSupport.DEFAULT_SEVERITY); + setIssueSeverityForUnknownCodeSystem(UnknownCodeSystemWarningValidationSupport.DEFAULT_SEVERITY); } + + @Test public void testValidateCodeInValueSetWithUnknownCodeSystem_FailValidation() { createStructureDefWithBindingToUnknownCs(true); @@ -171,23 +174,26 @@ public void testValidateCodeInValueSetWithUnknownCodeSystem_FailValidation() { oo = validateAndReturnOutcome(obs); String encoded = encode(oo); ourLog.info(encoded); - assertThat(oo.getIssueFirstRep().getDiagnostics()).as(encoded).isEqualTo("No issues detected during validation"); + assertThat(oo.getIssue()).hasSize(1); + assertThat(oo.getIssueFirstRep().getDiagnostics()).as(encoded).isEqualTo("CodeSystem is unknown and can't be validated: http://cs for 'http://cs#code1'"); + assertThat(oo.getIssueFirstRep().getSeverity()).as(encoded).isEqualTo(OperationOutcome.IssueSeverity.ERROR); // Invalid code obs.setValue(new Quantity().setSystem("http://cs").setCode("code99").setValue(123)); oo = validateAndReturnOutcome(obs); encoded = encode(oo); ourLog.info(encoded); - assertThat(oo.getIssue().size()).as(encoded).isEqualTo(1); - assertThat(oo.getIssue().get(0).getDiagnostics()).contains("provided (http://cs#code99) was not found in the value set"); - assertThat(oo.getIssue().get(0).getDiagnostics()).contains("Unknown code 'http://cs#code99' for in-memory expansion of ValueSet 'http://vs'"); - assertThat(oo.getIssueFirstRep().getSeverity()).as(encoded).isEqualTo(OperationOutcome.IssueSeverity.ERROR); - + assertThat(oo.getIssue().size()).as(encoded).isEqualTo(2); + assertThat(oo.getIssue().get(0).getDiagnostics()).as(encoded).isEqualTo("CodeSystem is unknown and can't be validated: http://cs for 'http://cs#code99'"); + assertThat(oo.getIssue().get(0).getSeverity()).as(encoded).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(oo.getIssue().get(1).getDiagnostics()).contains("provided (http://cs#code99) was not found in the value set"); + assertThat(oo.getIssue().get(1).getDiagnostics()).contains("Unknown code 'http://cs#code99' for in-memory expansion of ValueSet 'http://vs'"); + assertThat(oo.getIssue().get(1).getSeverity()).as(encoded).isEqualTo(OperationOutcome.IssueSeverity.ERROR); } @Test public void testValidateCodeInEnumeratedValueSetWithUnknownCodeSystem_Information() { - myUnknownCodeSystemWarningValidationSupport.setNonExistentCodeSystemSeverity(IValidationSupport.IssueSeverity.INFORMATION); + setIssueSeverityForUnknownCodeSystem(IValidationSupport.IssueSeverity.INFORMATION); createStructureDefWithBindingToUnknownCs(true); @@ -202,7 +208,7 @@ public void testValidateCodeInEnumeratedValueSetWithUnknownCodeSystem_Informatio encoded = encode(oo); ourLog.info(encoded); assertThat(oo.getIssue()).hasSize(1); - assertEquals("No issues detected during validation", oo.getIssueFirstRep().getDiagnostics()); + assertThat(oo.getIssue().get(0).getDiagnostics()).contains("CodeSystem is unknown and can't be validated: http://cs for 'http://cs#code1'"); assertEquals(OperationOutcome.IssueSeverity.INFORMATION, oo.getIssueFirstRep().getSeverity()); // Invalid code @@ -210,10 +216,12 @@ public void testValidateCodeInEnumeratedValueSetWithUnknownCodeSystem_Informatio oo = validateAndReturnOutcome(obs, true); encoded = encode(oo); ourLog.info(encoded); - assertThat(oo.getIssue()).hasSize(1); - assertThat(oo.getIssueFirstRep().getDiagnostics()).contains("provided (http://cs#code99) was not found in the value set"); - assertThat(oo.getIssueFirstRep().getDiagnostics()).contains("Unknown code 'http://cs#code99' for in-memory expansion of ValueSet 'http://vs'"); - assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssueFirstRep().getSeverity()); + assertThat(oo.getIssue()).hasSize(2); + assertThat(oo.getIssue().get(0).getDiagnostics()).contains("CodeSystem is unknown and can't be validated: http://cs for 'http://cs#code99'"); + assertThat(oo.getIssue().get(0).getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + assertThat(oo.getIssue().get(1).getDiagnostics()).contains("provided (http://cs#code99) was not found in the value set"); + assertThat(oo.getIssue().get(1).getDiagnostics()).contains("Unknown code 'http://cs#code99' for in-memory expansion of ValueSet 'http://vs'"); + assertThat(oo.getIssue().get(1).getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); } /** @@ -222,7 +230,7 @@ public void testValidateCodeInEnumeratedValueSetWithUnknownCodeSystem_Informatio @Test public void testValidateCodeInEnumeratedValueSetWithUnknownCodeSystem_Warning() { // set to warning - myUnknownCodeSystemWarningValidationSupport.setNonExistentCodeSystemSeverity(IValidationSupport.IssueSeverity.WARNING); + setIssueSeverityForUnknownCodeSystem(IValidationSupport.IssueSeverity.WARNING); createStructureDefWithBindingToUnknownCs(true); @@ -254,7 +262,7 @@ public void testValidateCodeInEnumeratedValueSetWithUnknownCodeSystem_Warning() @Test public void testValidateCodeInEnumeratedValueSetWithUnknownCodeSystem_Error() { - myUnknownCodeSystemWarningValidationSupport.setNonExistentCodeSystemSeverity(IValidationSupport.IssueSeverity.ERROR); + setIssueSeverityForUnknownCodeSystem(IValidationSupport.IssueSeverity.ERROR); createStructureDefWithBindingToUnknownCs(true); @@ -274,27 +282,34 @@ public void testValidateCodeInEnumeratedValueSetWithUnknownCodeSystem_Error() { // Valid code obs.setValue(new Quantity().setSystem("http://cs").setCode("code1").setValue(123)); - oo = validateAndReturnOutcome(obs, false); + // The code system is unknown, we should expect an error for that. + // But we don't expect an error related to the code not being in the value set, since it is a valid code in the value set. + oo = validateAndReturnOutcome(obs, true); encoded = encode(oo); ourLog.info(encoded); assertThat(oo.getIssue()).hasSize(1); - assertThat(oo.getIssueFirstRep().getDiagnostics()).contains("No issues detected during validation"); - assertEquals(OperationOutcome.IssueSeverity.INFORMATION, oo.getIssueFirstRep().getSeverity()); + assertThat(oo.getIssueFirstRep().getDiagnostics()).contains("CodeSystem is unknown and can't be validated: http://cs for 'http://cs#code1'"); + assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssueFirstRep().getSeverity()); // Invalid code obs.setValue(new Quantity().setSystem("http://cs").setCode("code99").setValue(123)); oo = validateAndReturnOutcome(obs, true); encoded = encode(oo); ourLog.info(encoded); - assertThat(oo.getIssue()).hasSize(1); - assertThat(oo.getIssue().get(0).getDiagnostics()).contains("provided (http://cs#code99) was not found in the value set"); - assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssueFirstRep().getSeverity()); + assertThat(oo.getIssue()).hasSize(2); + assertThat(oo.getIssue().get(0).getDiagnostics()).contains("CodeSystem is unknown and can't be validated: http://cs for 'http://cs#code99'"); + assertThat(oo.getIssue().get(0).getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(oo.getIssue().get(1).getDiagnostics()).contains("provided (http://cs#code99) was not found in the value set"); + assertThat(oo.getIssue().get(1).getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); } - @Test - public void testValidateCodeInNonEnumeratedValueSetWithUnknownCodeSystem_Information() { - myUnknownCodeSystemWarningValidationSupport.setNonExistentCodeSystemSeverity(IValidationSupport.IssueSeverity.INFORMATION); + @ParameterizedTest + @CsvSource({ + "information", "warning", "error" + }) + public void testValidateCodeInNonEnumeratedValueSetWithUnknownCodeSystem(String theUnknownCodeSeverity) { + setIssueSeverityForUnknownCodeSystem(IValidationSupport.IssueSeverity.fromCode(theUnknownCodeSeverity)); createStructureDefWithBindingToUnknownCs(false); @@ -303,107 +318,39 @@ public void testValidateCodeInNonEnumeratedValueSetWithUnknownCodeSystem_Informa OperationOutcome oo; String encoded; - // Valid code + // in this test setup, the value set doesn't contain any enumerated codes, + // and the http://cs is an unknown system so the code provided here cannot be validated in any way. obs.setValue(new Quantity().setSystem("http://cs").setCode("code1").setValue(123)); - oo = validateAndReturnOutcome(obs, false); + boolean expectError = "error".equals(theUnknownCodeSeverity); + oo = validateAndReturnOutcome(obs, expectError); encoded = encode(oo); ourLog.info(encoded); - assertThat(oo.getIssue()).hasSize(1); - assertEquals("No issues detected during validation", oo.getIssueFirstRep().getDiagnostics()); - assertEquals(OperationOutcome.IssueSeverity.INFORMATION, oo.getIssueFirstRep().getSeverity()); - // Invalid code - obs.setValue(new Quantity().setSystem("http://cs").setCode("code99").setValue(123)); - oo = validateAndReturnOutcome(obs, false); - encoded = encode(oo); - ourLog.info(encoded); - assertThat(oo.getIssue()).hasSize(1); - assertEquals("No issues detected during validation", oo.getIssueFirstRep().getDiagnostics()); - assertEquals(OperationOutcome.IssueSeverity.INFORMATION, oo.getIssueFirstRep().getSeverity()); - } - - /** - * By default, an unknown code system should fail validation - */ - @Test - public void testValidateCodeInNonEnumeratedValueSetWithUnknownCodeSystem_Warning() { - // set to warning - myUnknownCodeSystemWarningValidationSupport.setNonExistentCodeSystemSeverity(IValidationSupport.IssueSeverity.WARNING); - - createStructureDefWithBindingToUnknownCs(false); - - Observation obs = createObservationForUnknownCodeSystemTest(); + assertThat(oo.getIssue()).hasSize(3); - OperationOutcome oo; - String encoded; + OperationOutcome.IssueSeverity expectedSeverity = OperationOutcome.IssueSeverity.fromCode(theUnknownCodeSeverity); - // Valid code - obs.setValue(new Quantity().setSystem("http://cs").setCode("code1").setValue(123)); - oo = validateAndReturnOutcome(obs, false); - encoded = encode(oo); - ourLog.info(encoded); - assertThat(oo.getIssue()).hasSize(1); - assertEquals("CodeSystem is unknown and can't be validated: http://cs for 'http://cs#code1'", oo.getIssueFirstRep().getDiagnostics()); - assertEquals(OperationOutcome.IssueSeverity.WARNING, oo.getIssueFirstRep().getSeverity()); - - // Invalid code - obs.setValue(new Quantity().setSystem("http://cs").setCode("code99").setValue(123)); - oo = validateAndReturnOutcome(obs, false); - encoded = encode(oo); - ourLog.info(encoded); - assertThat(oo.getIssue()).hasSize(1); - assertEquals("CodeSystem is unknown and can't be validated: http://cs for 'http://cs#code99'", oo.getIssue().get(0).getDiagnostics()); - assertEquals(OperationOutcome.IssueSeverity.WARNING, oo.getIssue().get(0).getSeverity()); - } - - @Test - public void testValidateCodeInNonEnumeratedValueSetWithUnknownCodeSystem_Error() { - myUnknownCodeSystemWarningValidationSupport.setNonExistentCodeSystemSeverity(IValidationSupport.IssueSeverity.ERROR); - - createStructureDefWithBindingToUnknownCs(false); - - Observation obs = new Observation(); - obs.getMeta().addProfile("http://sd"); - obs.getText().setDivAsString("
Hello
"); - obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED); - obs.getCategoryFirstRep().addCoding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("vital-signs"); - obs.getCode().setText("hello"); - obs.setSubject(new Reference("Patient/123")); - obs.addPerformer(new Reference("Practitioner/123")); - obs.setEffective(DateTimeType.now()); - obs.setStatus(ObservationStatus.FINAL); - - OperationOutcome oo; - String encoded; - - // Valid code - obs.setValue(new Quantity().setSystem("http://cs").setCode("code1").setValue(123)); - oo = validateAndReturnOutcome(obs, true); - encoded = encode(oo); - ourLog.info(encoded); - assertThat(oo.getIssue()).hasSize(2); OperationOutcome.OperationOutcomeIssueComponent unableToExpandError = oo.getIssue().get(0); assertThat(unableToExpandError.getDiagnostics()).contains("Unable to expand ValueSet because CodeSystem could not be found: http://cs"); - assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssueFirstRep().getSeverity()); + assertEquals(expectedSeverity, unableToExpandError.getSeverity()); - OperationOutcome.OperationOutcomeIssueComponent notInValueSetError = oo.getIssue().get(1); - assertThat(notInValueSetError.getDiagnostics()).contains("provided (http://cs#code1) was not found in the value set"); + OperationOutcome.OperationOutcomeIssueComponent unknownCodeSystemError = oo.getIssue().get(1); + assertThat(unknownCodeSystemError.getDiagnostics()).contains("CodeSystem is unknown and can't be validated: http://cs for 'http://cs#code1'"); + assertEquals(expectedSeverity, unknownCodeSystemError.getSeverity()); + + OperationOutcome.OperationOutcomeIssueComponent notInValueSetError = oo.getIssue().get(2); assertThat(notInValueSetError.getDiagnostics()).contains("Failed to expand ValueSet 'http://vs' (in-memory). Could not validate code http://cs#code1"); assertThat(notInValueSetError.getDiagnostics()).contains("HAPI-0702: Unable to expand ValueSet because CodeSystem could not be found: http://cs"); - assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssueFirstRep().getSeverity()); assertEquals(27, ((IntegerType) notInValueSetError.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-line").getValue()).getValue()); assertEquals(4, ((IntegerType) notInValueSetError.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-col").getValue()).getValue()); - assertEquals("Terminology_TX_NoValid_12", ((StringType) notInValueSetError.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id").getValue()).getValue()); assertEquals(OperationOutcome.IssueType.PROCESSING, notInValueSetError.getCode()); - assertEquals(OperationOutcome.IssueSeverity.ERROR, notInValueSetError.getSeverity()); + assertEquals(expectedSeverity, notInValueSetError.getSeverity()); assertThat(notInValueSetError.getLocation()).hasSize(2); assertEquals("Observation.value.ofType(Quantity)", notInValueSetError.getLocation().get(0).getValue()); assertEquals("Line[27] Col[4]", notInValueSetError.getLocation().get(1).getValue()); - } - private Observation createObservationForUnknownCodeSystemTest() { Observation obs = new Observation(); obs.getMeta().addProfile("http://sd"); @@ -421,7 +368,7 @@ private Observation createObservationForUnknownCodeSystemTest() { @Test public void testValidateCodeInValueSet_InferredCodeSystem_WarningOnUnknown() { // set to warning - myUnknownCodeSystemWarningValidationSupport.setNonExistentCodeSystemSeverity(IValidationSupport.IssueSeverity.WARNING); + setIssueSeverityForUnknownCodeSystem(IValidationSupport.IssueSeverity.WARNING); OperationOutcome oo; String encoded; @@ -441,7 +388,7 @@ public void testValidateCodeInValueSet_InferredCodeSystem_WarningOnUnknown() { @Test public void testValidateCodeInValueSet_InferredCodeSystem_ErrorOnUnknown() { // set to warning - myUnknownCodeSystemWarningValidationSupport.setNonExistentCodeSystemSeverity(IValidationSupport.IssueSeverity.ERROR); + setIssueSeverityForUnknownCodeSystem(IValidationSupport.IssueSeverity.ERROR); OperationOutcome oo; String encoded; @@ -666,6 +613,7 @@ public void testValidateCode_InMemoryExpansionAgainstHugeValueSet() throws Excep OperationOutcome oo; // Valid code + obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED); obs.getCode().getCodingFirstRep().setSystem("http://loinc.org").setCode("CODE3").setDisplay("Display 3"); oo = validateAndReturnOutcome(obs); @@ -683,11 +631,14 @@ public void testValidateCode_InMemoryExpansionAgainstHugeValueSet() throws Excep oo = validateAndReturnOutcome(obs); assertThat(encode(oo)).contains("None of the codings provided are in the value set 'ValueSet[http://example.com/fhir/ValueSet/observation-vitalsignresult]' (http://example.com/fhir/ValueSet/observation-vitalsignresult)"); + // Valid code with wrong system obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED); obs.getCode().getCodingFirstRep().setSystem("http://foo").setCode("CODE3").setDisplay("Display 3"); oo = validateAndReturnOutcome(obs); - assertThat(oo.getIssueFirstRep().getDiagnostics()).as(encode(oo)).isEqualTo("None of the codings provided are in the value set 'ValueSet[http://example.com/fhir/ValueSet/observation-vitalsignresult]' (http://example.com/fhir/ValueSet/observation-vitalsignresult), and a coding from this value set is required) (codes = http://foo#CODE3)"); + assertThat(oo.getIssue().get(0).getDiagnostics()).as(encode(oo)).isEqualTo("CodeSystem is unknown and can't be validated: http://foo for 'http://foo#CODE3'"); + assertThat(oo.getIssue().get(1).getDiagnostics()).as(encode(oo)).isEqualTo("None of the codings provided are in the value set 'ValueSet[http://example.com/fhir/ValueSet/observation-vitalsignresult]' (http://example.com/fhir/ValueSet/observation-vitalsignresult), and a coding from this value set is required) (codes = http://foo#CODE3)"); + // Code that exists but isn't in the valueset obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED); @@ -719,13 +670,12 @@ public void testValidateCode_InMemoryExpansionAgainstHugeValueSet() throws Excep oo = validateAndReturnOutcome(obs); assertThat(oo.getIssueFirstRep().getDiagnostics()).as(encode(oo)).isEqualTo("No issues detected during validation"); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - - } @ParameterizedTest @MethodSource("paramsUnknownCodeSystemBindingStrengths") - void testValidateCode_unknownCodeSystem_returnsCorrectSeverityForDifferentBindingStrengths(Enumerations.BindingStrength theStructureDefinitionStrength, int theExpectedNumIssues, String theExpectedSeverity, String theExpectedDiagnosticsMessage) throws Exception { + void testValidateCode_unknownCodeSystem_returnsCorrectSeverityForDifferentBindingStrengths(Enumerations.BindingStrength theStructureDefinitionStrength, int theExpectedNumIssues, String theExpectedSeverity, List theExpectedDiagnosticsMessages) throws Exception { + // Given myStorageSettings.setPreExpandValueSets(true); @@ -756,23 +706,71 @@ void testValidateCode_unknownCodeSystem_returnsCorrectSeverityForDifferentBindin // When oo = validateAndReturnOutcome(org); + String encoded = encode(oo); + ourLog.info("HERE:" + encoded); // Then assertThat(oo.getIssue()).hasSize(theExpectedNumIssues); - assertThat(oo.getIssueFirstRep().getSeverity().getDisplay()).isEqualTo(theExpectedSeverity); - assertThat(oo.getIssueFirstRep().getDiagnostics()).isEqualTo(theExpectedDiagnosticsMessage); + int i = 0; + for (String theExpectedMessage : theExpectedDiagnosticsMessages) { + OperationOutcome.OperationOutcomeIssueComponent issue = oo.getIssue().get(i++); + assertThat(issue.getSeverity().getDisplay()).isEqualTo(theExpectedSeverity); + assertThat(issue.getDiagnostics()).contains(theExpectedMessage); + } } private static Stream paramsUnknownCodeSystemBindingStrengths() { - String expectedErrorMessage = "Unable to validate code http://mylocalcodesystem#mylocalcode - No codes in ValueSet belong to CodeSystem with URL http://mylocalcodesystem"; - String expectedSuccessMessage = "No issues detected during validation"; + String codingIsNotInValueSetMessageWithBindingSpecificReason ="None of the codings provided are in the value set 'ValueSet[http://mytest/ValueSet/OrgContactSampleVS]' (http://mytest/ValueSet/OrgContactSampleVS), and"; + String unknownCodeSystemMessage = "CodeSystem is unknown and can't be validated: http://mylocalcodesystem for 'http://mylocalcodesystem#mylocalcode'"; + String unableToValidateCodeMessage = "Unable to validate code http://mylocalcodesystem#mylocalcode - No codes in ValueSet belong to CodeSystem with URL http://mylocalcodesystem"; + + // The core validator has its own logic to determine the severity of not_found systems/codes based on the binding strength. + // It usually sets the severity to Error for REQUIRED bindings and Warning for others. + // So even when we configure our own validator to generate an Error, the core validator will downgrade some of them to Warnings. + // That is the reason why in this test the severity for unknown code systems is not set and hence defaults to Error, but the actual severity is set by the core validator and is only error the required case, and warnings for the other cases. + // see for example: https://github.com/hapifhir/org.hl7.fhir.core/blob/1e148bf3d303f360556654c6586388a582f4fd4c/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java#L1884 + // and the related question: https://github.com/hapifhir/org.hl7.fhir.core/issues/2129 return Stream.of( - Arguments.of(Enumerations.BindingStrength.REQUIRED, 2, "Error", expectedErrorMessage), - Arguments.of(Enumerations.BindingStrength.EXTENSIBLE, 2, "Warning", expectedErrorMessage), - Arguments.of(Enumerations.BindingStrength.PREFERRED, 1, "Warning", expectedErrorMessage), - Arguments.of(Enumerations.BindingStrength.EXAMPLE, 1, "Information", expectedSuccessMessage) + Arguments.of(Enumerations.BindingStrength.REQUIRED, 3, "Error", List.of(unableToValidateCodeMessage, unknownCodeSystemMessage, codingIsNotInValueSetMessageWithBindingSpecificReason)), + Arguments.of(Enumerations.BindingStrength.EXTENSIBLE, 3, "Warning", List.of(unableToValidateCodeMessage, unknownCodeSystemMessage, codingIsNotInValueSetMessageWithBindingSpecificReason)), + Arguments.of(Enumerations.BindingStrength.PREFERRED, 2, "Warning", List.of(unableToValidateCodeMessage, unknownCodeSystemMessage)), + Arguments.of(Enumerations.BindingStrength.EXAMPLE, 1, "Warning", List.of(unknownCodeSystemMessage)) ); } + @ParameterizedTest + @CsvSource({"error", "warning", "information"}) + @Disabled(""" + This test is disabled because it fails for the "error" severity. + The core validator has its own logic to determine the severity of not_found system based on the binding strength. + It sets the severity to Error for REQUIRED bindings and Warning for others. + So even when we configure our own validator to generate an Error for an unknown system, the core validator downgrades it a warning. + see for example: https://github.com/hapifhir/org.hl7.fhir.core/blob/1e148bf3d303f360556654c6586388a582f4fd4c/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java#L1884 + and the related question: https://github.com/hapifhir/org.hl7.fhir.core/issues/2129 + Depending on the outcome of that discussion, we may need to change the test to expect a warning instead of an error when the binding strength is not "required" and update the documentation accordingly. + """) + public void testValidateCode_UnknownCodeSystem_ExampleBindingStrength_whenUnknownIssueSeverityConfigured(String theIssueSeverityCode) { + setIssueSeverityForUnknownCodeSystem(IValidationSupport.IssueSeverity.fromCode(theIssueSeverityCode)); + + Medication medication = new Medication(); + // the Medication.code has anbinding to http://snomed.info/sct with "example" strength in the FHIR spec + medication.setCode(new CodeableConcept().addCoding(new Coding("http://unknown-system", "169008", "Product containing hypothalamic releasing factor"))); + medication.getText().setDiv(new XhtmlNode().setValue("
AA
")).setStatus(Narrative.NarrativeStatus.GENERATED); + + OperationOutcome oo; + String encoded; + + // execute + oo = validateAndReturnOutcome(medication); + + // verify + encoded = encode(oo); + ourLog.info(encoded); + assertThat(oo.getIssue()).hasSize(1); + assertThat(oo.getIssueFirstRep().getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.fromCode(theIssueSeverityCode)); + assertThat(oo.getIssueFirstRep().getDiagnostics()).contains("CodeSystem is unknown and can't be validated: http://unknown-system for 'http://unknown-system#169008'"); + } + + @Test public void testValidateProfileTargetType_PolicyCheckValid() throws IOException { myValidationSettings.setLocalReferenceValidationDefaultPolicy(ReferenceValidationPolicy.CHECK_VALID); @@ -1110,7 +1108,8 @@ public void testValidateWithFragmentCodeSystem_WithDirectBinding() throws IOExce outcome = (OperationOutcome) myObservationDao.validate(obs, null, null, null, ValidationModeEnum.CREATE, "http://example.com/structuredefinition", mySrd).getOperationOutcome(); assertHasErrors(outcome); ourLog.debug("Outcome: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); - assertThat(outcome.getIssueFirstRep().getDiagnostics()).contains("None of the codings provided are in the value set 'MessageCategory'"); + assertThat(outcome.getIssue().get(0).getDiagnostics()).contains("CodeSystem is unknown and can't be validated: http://example.com/foo-foo for 'http://example.com/foo-foo#some-code'"); + assertThat(outcome.getIssue().get(1).getDiagnostics()).contains("None of the codings provided are in the value set 'MessageCategory'"); assertEquals(OperationOutcome.IssueSeverity.ERROR, outcome.getIssueFirstRep().getSeverity()); } @@ -1194,7 +1193,8 @@ public void testValidateCode_PreExpansionAgainstHugeValueSet() throws Exception obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED); obs.getCode().getCodingFirstRep().setSystem("http://foo").setCode("CODE3").setDisplay("Display 3"); oo = validateAndReturnOutcome(obs); - assertThat(oo.getIssue().get(1).getDiagnostics()).as(encode(oo)).isEqualTo("None of the codings provided are in the value set 'ValueSet[http://example.com/fhir/ValueSet/observation-vitalsignresult]' (http://example.com/fhir/ValueSet/observation-vitalsignresult), and a coding from this value set is required) (codes = http://foo#CODE3)"); + assertThat(oo.getIssue().get(1).getDiagnostics()).as(encode(oo)).contains("CodeSystem is unknown and can't be validated: http://foo for 'http://foo#CODE3'"); + assertThat(oo.getIssue().get(2).getDiagnostics()).as(encode(oo)).isEqualTo("None of the codings provided are in the value set 'ValueSet[http://example.com/fhir/ValueSet/observation-vitalsignresult]' (http://example.com/fhir/ValueSet/observation-vitalsignresult), and a coding from this value set is required) (codes = http://foo#CODE3)"); // Code that exists but isn't in the valueset obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED); @@ -1215,8 +1215,16 @@ public void testValidateCode_PreExpansionAgainstHugeValueSet() throws Exception * Make sure that we do something sane when validating throws an unexpected exception */ @Test + @Disabled(""" + I've disabled this test because it doesn't test what it claims to test since it was created, and it was passing for the wrong reason. + It looks like it is supposed test what happens when a ValidationSupport class throws exception on validateCodeInValueSet, + but the stubbed method catches and suppresses the exception it is supposed to throw, and returns null. + Not clear what is the expected behavior here, but the SupportChain doesn't have any logic to suppress the exceptions thrown + by a ValidationSupport on validateCodeInValueSet, so the current behaviour is that whatever exception thrown by a ValidationSupport bubbles up to the caller. + """) public void testValidate_ValidationSupportThrowsException() { IValidationSupport validationSupport = mock(IValidationSupport.class); + when(validationSupport.isValueSetSupported(any(), any())).thenReturn(true); when(validationSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any())).thenAnswer(t -> { // This will fail with a constraint error try { @@ -1357,6 +1365,7 @@ private OperationOutcome validateAndReturnOutcome(T th String encoded = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(theObs); MethodOutcome outcome = dao.validate(theObs, null, encoded, EncodingEnum.JSON, ValidationModeEnum.CREATE, null, mySrd); OperationOutcome oo = (OperationOutcome) outcome.getOperationOutcome(); + ourLog.info("EMRE:" + encode(oo)); if (theWantError) { assertHasErrors(oo); } else { @@ -2219,7 +2228,10 @@ public void testValidateCodeInUnknownCodeSystemWithRequiredBinding() throws IOEx OperationOutcome oo = (OperationOutcome) result.getOperationOutcome(); assertHasErrors(oo); ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo)); - assertThat(oo.getIssueFirstRep().getDiagnostics()).contains("None of the codings provided are in the value set 'Condition Clinical Status Codes' (http://hl7.org/fhir/ValueSet/condition-clinical|4.0.1), and a coding from this value set is required) (codes = http://terminology.hl7.org/CodeSystem/condition-clinical/wrong-system#notrealcode)"); + assertThat(oo.getIssue().get(0).getDiagnostics()).contains("CodeSystem is unknown and can't be validated: http://terminology.hl7.org/CodeSystem/condition-clinical/wrong-system for 'http://terminology.hl7.org/CodeSystem/condition-clinical/wrong-system#notrealcode"); + assertThat(oo.getIssue().get(0).getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(oo.getIssue().get(1).getDiagnostics()).contains("None of the codings provided are in the value set 'Condition Clinical Status Codes' (http://hl7.org/fhir/ValueSet/condition-clinical|4.0.1), and a coding from this value set is required) (codes = http://terminology.hl7.org/CodeSystem/condition-clinical/wrong-system#notrealcode)"); + assertThat(oo.getIssue().get(1).getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); } private IBaseResource findResourceByIdInBundle(Bundle vss, String name) { @@ -2494,4 +2506,15 @@ public void testValidateObservationWithVitalSignsLoincCode() { assertHasNoErrors(oo); } + private void setIssueSeverityForUnknownCodeSystem(IValidationSupport.IssueSeverity theSeverity) { + myStorageSettings.setIssueSeverityForUnknownCodeSystem(theSeverity); + // in the JpaServer, these two below would be the set based on the storage configuration upon their instantiation, + // however, in individual tests these objects are already instantiated when the test method starts, + // I could try to have nested test classes for each severity case, but that would make my changes more difficult to review + // and see what I actually changed in the tests so instead I set the severity here directly for now, for the first iteration. + myUnknownCodeSystemWarningValidationSupport.setNonExistentCodeSystemSeverity(theSeverity); + myInMemoryTerminologyServerValidationSupport.setIssueSeverityForUnknownCodeSystem(theSeverity); + } + } + diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/RemoteTerminologyServiceJpaR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/RemoteTerminologyServiceJpaR4Test.java index 773e65eddd38..ba3beca1e1e6 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/RemoteTerminologyServiceJpaR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/RemoteTerminologyServiceJpaR4Test.java @@ -309,12 +309,13 @@ public void testValidateMultipleCodings() { "None of the codings provided are in the value set 'IdentifierType'"); // Verify 1 - Assertions.assertEquals(2, myCaptureQueriesListener.countGetConnections()); + Assertions.assertEquals(3, myCaptureQueriesListener.countGetConnections()); assertThat(ourValueSetProvider.mySearchUrls).asList().containsExactlyInAnyOrder( "http://hl7.org/fhir/ValueSet/identifier-type", "http://hl7.org/fhir/ValueSet/identifier-type" ); assertThat(ourCodeSystemProvider.mySearchUrls).asList().containsExactlyInAnyOrder( + "http://terminology.hl7.org/CodeSystem/v2-0203", "http://terminology.hl7.org/CodeSystem/v2-0203", "http://terminology.hl7.org/CodeSystem/v2-0203" ); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/validation/performance/ValidationCanonicalizationTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/validation/performance/ValidationCanonicalizationTest.java index 2719fa53267a..b2b7bf11bd78 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/validation/performance/ValidationCanonicalizationTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/validation/performance/ValidationCanonicalizationTest.java @@ -148,22 +148,27 @@ public void testCanonicalization() { private void assertHasInvalidCodeError(OperationOutcome theOperationOutcome) { List issues = theOperationOutcome.getIssue(); - assertEquals(3, issues.size()); + assertEquals(4, issues.size()); - OperationOutcomeIssueComponent issue1 = issues.get(0); + OperationOutcomeIssueComponent issue0 = issues.get(0); + assertEquals(IssueSeverity.ERROR, issue0.getSeverity()); + String expectedMessage0 = "CodeSystem is unknown and can't be validated: http://acme.org/invalid for 'http://acme.org/invalid#invalid'"; + assertEquals(expectedMessage0, issue0.getDiagnostics()); + + OperationOutcomeIssueComponent issue1 = issues.get(1); assertEquals(IssueSeverity.ERROR, issue1.getSeverity()); assertEquals("Parameters.parameter[0].resource/*Procedure/null*/.code", issue1.getLocation().get(0).getValue()); Map formatValues = Map.of("conceptNumber", String.valueOf(NUM_CONCEPTS)); String expectedMessage1 = "None of the codings provided are in the value set 'Value Set Combined' (http://acme.org/ValueSet/valueset-combined|1), and a coding from this value set is required) (codes = http://acme.org/CodeSystem/codesystem-1#codesystem-1-concept-${conceptNumber}, http://acme.org/CodeSystem/codesystem-2#codesystem-2-concept-${conceptNumber}, http://acme.org/invalid#invalid)"; assertEquals(formatMessage(expectedMessage1, formatValues), issue1.getDiagnostics()); - OperationOutcomeIssueComponent issue2 = issues.get(1); + OperationOutcomeIssueComponent issue2 = issues.get(2); assertEquals(IssueSeverity.INFORMATION, issue2.getSeverity()); assertEquals("Parameters.parameter[0].resource/*Procedure/null*/.code.coding[2]", issue2.getLocation().get(0).getValue()); String expectedMessage2 = "This element does not match any known slice defined in the profile http://example.org/fhir/StructureDefinition/TestProcedure|1.0.0 (this may not be a problem, but you should check that it's not intended to match a slice) - Does not match slice 'slice1' (discriminator: ($this memberOf 'http://acme.org/ValueSet/valueset-1'))"; assertEquals(expectedMessage2, issue2.getDiagnostics()); - OperationOutcomeIssueComponent issue3 = issues.get(2); + OperationOutcomeIssueComponent issue3 = issues.get(3); assertEquals(IssueSeverity.INFORMATION, issue3.getSeverity()); assertEquals("Parameters.parameter[0].resource/*Procedure/null*/.code.coding[2]", issue3.getLocation().get(0).getValue()); String expectedMessage3 = "This element does not match any known slice defined in the profile http://example.org/fhir/StructureDefinition/TestProcedure|1.0.0 (this may not be a problem, but you should check that it's not intended to match a slice) - Does not match slice 'slice2' (discriminator: ($this memberOf 'http://acme.org/ValueSet/valueset-2'))"; diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateCodeWithRemoteTerminologyR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateCodeWithRemoteTerminologyR4Test.java index 78088c6a3b84..f8bf3c601656 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateCodeWithRemoteTerminologyR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateCodeWithRemoteTerminologyR4Test.java @@ -140,8 +140,7 @@ public void validateCodeOperationOnCodeSystem_byCodingAndUrlWhereCodeSystemIsUnk ourLog.info(resp); assertFalse(((BooleanType) respParam.getParameterValue("result")).booleanValue()); - assertThat(respParam.getParameterValue("message").toString()).isEqualTo("Terminology service was unable to provide validation for " + INVALID_CODE_SYSTEM_URI + - "#P"); + assertThat(respParam.getParameterValue("message").toString()).isEqualTo("CodeSystem is unknown and can't be validated: %s for '%s%s'", INVALID_CODE_SYSTEM_URI, INVALID_CODE_SYSTEM_URI, "#P"); } @Test @@ -216,6 +215,7 @@ public void validateCodeOperationOnValueSet_byUrlSystemAndCode() { @Test public void validateCodeOperationOnValueSet_byCodingAndUrlWhereValueSetIsUnknown_returnsFalse() { myValueSetProvider.setShouldThrowExceptionForResourceNotFound(false); + myCodeSystemProvider.setShouldThrowExceptionForResourceNotFound(false); Parameters respParam = myClient .operation() diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java index 0b07069e2170..7373c5b48d80 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java @@ -363,6 +363,13 @@ public class JpaStorageSettings extends StorageSettings { private IValidationSupport.IssueSeverity myIssueSeverityForCodeDisplayMismatch = IValidationSupport.IssueSeverity.WARNING; + /** + * @since 7.0.0 + */ + @Nonnull + private IValidationSupport.IssueSeverity myIssueSeverityForUnknownCodeSystem = + IValidationSupport.IssueSeverity.ERROR; + /** * This setting allows preventing a conditional update to invalidate the match criteria. *

@@ -2615,6 +2622,30 @@ public void setIssueSeverityForCodeDisplayMismatch( myIssueSeverityForCodeDisplayMismatch = theIssueSeverityForCodeDisplayMismatch; } + /** + * This setting controls the validation issue severity to report when a code validation + * encounters an unknown CodeSystem. Defaults to {@link IValidationSupport.IssueSeverity#ERROR}. + * + * @since 8.6.0 + */ + @Nonnull + public IValidationSupport.IssueSeverity getIssueSeverityForUnknownCodeSystem() { + return myIssueSeverityForUnknownCodeSystem; + } + + /** + * This setting controls the validation issue severity to report when a code validation + * encounters an unknown CodeSystem. Defaults to {@link IValidationSupport.IssueSeverity#ERROR}. + * + * @param theIssueSeverityForUnknownCodeSystem The severity. Must not be {@literal null}. + * @since 8.6.0 + */ + public void setIssueSeverityForUnknownCodeSystem( + @Nonnull IValidationSupport.IssueSeverity theIssueSeverityForUnknownCodeSystem) { + Validate.notNull(theIssueSeverityForUnknownCodeSystem, "theIssueSeverityForUnknownCodeSystem must not be null"); + myIssueSeverityForUnknownCodeSystem = theIssueSeverityForUnknownCodeSystem; + } + /** * This method returns whether data will be stored in LOB columns as well as the columns * introduced to migrate away from LOB. Writing to LOB columns is set to false by diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java index 61611f54f9a9..858a03e27df2 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java @@ -52,6 +52,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu private final FhirContext myCtx; private VersionCanonicalizer myVersionCanonicalizer; private IssueSeverity myIssueSeverityForCodeDisplayMismatch = IssueSeverity.WARNING; + private IssueSeverity myIssueSeverityForUnknownCodeSystem = IssueSeverity.ERROR; /** * Constructor @@ -107,6 +108,30 @@ public void setIssueSeverityForCodeDisplayMismatch(@Nonnull IssueSeverity theIss myIssueSeverityForCodeDisplayMismatch = theIssueSeverityForCodeDisplayMismatch; } + /** + * This setting controls the validation issue severity to report when a code validation + * finds that the CodeSystem being validated is not known. + * Defaults to {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#ERROR}. + * + * @since 8.6.0 + */ + public IssueSeverity getIssueSeverityForUnknownCodeSystem() { + return myIssueSeverityForUnknownCodeSystem; + } + + /** + * This setting controls the validation issue severity to report when a code validation + * finds that the CodeSystem being validated is not known. + * Defaults to {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#ERROR}. + * + * @param theIssueSeverityForUnknownCodeSystem The severity. Must not be {@literal null}. + * @since 8.6.0 + */ + public void setIssueSeverityForUnknownCodeSystem(@Nonnull IssueSeverity theIssueSeverityForUnknownCodeSystem) { + Validate.notNull(theIssueSeverityForUnknownCodeSystem, "theIssueSeverityForUnknownCodeSystem must not be null"); + myIssueSeverityForUnknownCodeSystem = theIssueSeverityForUnknownCodeSystem; + } + @Override public FhirContext getFhirContext() { return myCtx; @@ -166,7 +191,12 @@ public CodeValidationResult validateCodeInValueSet( theValidationSupportContext, theValueSet, theCodeSystemUrlAndVersion, theCode); } catch (ExpansionCouldNotBeCompletedInternallyException e) { CodeValidationResult codeValidationResult = new CodeValidationResult(); - codeValidationResult.setSeverity(IssueSeverity.ERROR); + if (e.getCodeValidationIssue() != null && e.getCodeValidationIssue().getSeverity() != null) { + // preserve the severity from the original issue by assigning it to the result + codeValidationResult.setSeverity(e.getCodeValidationIssue().getSeverity()); + } else { + codeValidationResult.setSeverity(IssueSeverity.ERROR); + } String msg = "Failed to expand ValueSet '" + vsUrl + "' (in-memory). Could not validate code " + theCodeSystemUrlAndVersion + "#" + theCode; @@ -859,7 +889,7 @@ private boolean expandValueSetR5IncludeOrExclude( } boolean ableToHandleCode = false; - String failureMessage = null; + MessageWithSeverity failureMessage = null; boolean isIncludeCodeSystemIgnored = includeOrExcludeSystemResource != null && includeOrExcludeSystemResource.getContent() == Enumerations.CodeSystemContentMode.NOTPRESENT; @@ -991,15 +1021,15 @@ private boolean expandValueSetR5IncludeOrExclude( failureMessage = getFailureMessageForMissingOrUnusableCodeSystem( includeOrExcludeSystemResource, loadedCodeSystemUrl); } else { - failureMessage = "Unable to expand value set"; + failureMessage = new MessageWithSeverity("Unable to expand value set", IssueSeverity.ERROR); } } throw new ExpansionCouldNotBeCompletedInternallyException( - Msg.code(702) + failureMessage, + Msg.code(702) + failureMessage.message, new CodeValidationIssue( - failureMessage, - IssueSeverity.ERROR, + failureMessage.message, + failureMessage.severity, CodeValidationIssueCode.NOT_FOUND, CodeValidationIssueCoding.NOT_FOUND)); } @@ -1102,18 +1132,22 @@ private Function newCodeSystemLoader(ValidationSupportContex } } - private String getFailureMessageForMissingOrUnusableCodeSystem( + private record MessageWithSeverity(String message, IssueSeverity severity) {} + + private MessageWithSeverity getFailureMessageForMissingOrUnusableCodeSystem( CodeSystem includeOrExcludeSystemResource, String loadedCodeSystemUrl) { + IssueSeverity severity = IssueSeverity.ERROR; String failureMessage; if (includeOrExcludeSystemResource == null) { failureMessage = "Unable to expand ValueSet because CodeSystem could not be found: " + loadedCodeSystemUrl; + severity = myIssueSeverityForUnknownCodeSystem; } else { assert includeOrExcludeSystemResource.getContent() == Enumerations.CodeSystemContentMode.NOTPRESENT; failureMessage = "Unable to expand ValueSet because CodeSystem has CodeSystem.content=not-present but contents were not found: " + loadedCodeSystemUrl; } - return failureMessage; + return new MessageWithSeverity(failureMessage, severity); } private void addCodes( diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java index f89cd85a47da..f351fcd9f914 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java @@ -13,7 +13,7 @@ /** * This validation support module may be placed at the end of a {@link ValidationSupportChain} - * in order to configure the validator to generate a warning if a resource being validated + * in order to configure the validator to generate a warning or an error if a resource being validated * contains an unknown code system. * * Note that this module must also be activated by calling {@link #setAllowNonExistentCodeSystem(boolean)} @@ -45,18 +45,13 @@ public boolean isValueSetSupported(ValidationSupportContext theValidationSupport @Override public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { - return canValidateCodeSystem(theValidationSupportContext, theSystem); + return false; } @Nullable @Override public LookupCodeResult lookupCode( ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) { - // filters out error/fatal - if (canValidateCodeSystem(theValidationSupportContext, theLookupCodeRequest.getSystem())) { - return new LookupCodeResult().setFound(true); - } - return null; } @@ -68,8 +63,7 @@ public CodeValidationResult validateCode( String theCode, String theDisplay, String theValueSetUrl) { - // filters out error/fatal - if (!canValidateCodeSystem(theValidationSupportContext, theCodeSystem)) { + if (!canGenerateValidationResultForCodeSystem(theValidationSupportContext, theCodeSystem)) { return null; } @@ -80,19 +74,11 @@ public CodeValidationResult validateCode( + "#" + theCode + "'"; result.setMessage(theMessage); - // For information level, we just strip out the severity+message entirely - // so that nothing appears in the validation result - if (myNonExistentCodeSystemSeverity == IssueSeverity.INFORMATION) { - result.setCode(theCode); - result.setSeverity(null); - result.setMessage(null); - } else { - result.addIssue(new CodeValidationIssue( - theMessage, - myNonExistentCodeSystemSeverity, - CodeValidationIssueCode.NOT_FOUND, - CodeValidationIssueCoding.NOT_FOUND)); - } + result.addIssue(new CodeValidationIssue( + theMessage, + myNonExistentCodeSystemSeverity, + CodeValidationIssueCode.NOT_FOUND, + CodeValidationIssueCoding.NOT_FOUND)); return result; } @@ -106,7 +92,7 @@ public CodeValidationResult validateCodeInValueSet( String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) { - if (!canValidateCodeSystem(theValidationSupportContext, theCodeSystem)) { + if (!canGenerateValidationResultForCodeSystem(theValidationSupportContext, theCodeSystem)) { return null; } @@ -118,35 +104,11 @@ public CodeValidationResult validateCodeInValueSet( } /** - * Returns true if non existent code systems will still validate. - * False if they will throw errors. - * @return - */ - private boolean allowNonExistentCodeSystems() { - switch (myNonExistentCodeSystemSeverity) { - case ERROR: - case FATAL: - return false; - case WARNING: - case INFORMATION: - return true; - default: - ourLog.info("Unknown issue severity " + myNonExistentCodeSystemSeverity.name() - + ". Treating as INFO/WARNING"); - return true; - } - } - - /** - * Determines if the code system can (and should) be validated. - * @param theValidationSupportContext - * @param theCodeSystem - * @return + * If a validation support can fetch the code system, returns false. Otherwise, returns true. */ - private boolean canValidateCodeSystem(ValidationSupportContext theValidationSupportContext, String theCodeSystem) { - if (!allowNonExistentCodeSystems()) { - return false; - } + @Override + public boolean canGenerateValidationResultForCodeSystem( + ValidationSupportContext theValidationSupportContext, String theCodeSystem) { if (theCodeSystem == null) { return false; } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java index 8b4ecc0159cb..bd4c901083fc 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java @@ -772,6 +772,21 @@ private boolean isCodeSystemSupported( return value.getValue(); } + private boolean canGenerateValidationResultForCodeSystem( + ValidationSupportContext theValidationSupportContext, + IValidationSupport theValidationSupport, + String theCodeSystemUrl) { + CanGenerateValidationResultForCodeSystemKey key = + new CanGenerateValidationResultForCodeSystemKey(theValidationSupport, theCodeSystemUrl); + CacheValue value = getFromCache(key); + if (value == null) { + value = new CacheValue<>(theValidationSupport.canGenerateValidationResultForCodeSystem( + theValidationSupportContext, theCodeSystemUrl)); + putInCache(key, value); + } + return value.getValue(); + } + @Override public CodeValidationResult validateCode( @Nonnull ValidationSupportContext theValidationSupportContext, @@ -787,7 +802,9 @@ public CodeValidationResult validateCode( retVal = CacheValue.empty(); for (IValidationSupport next : myChain) { - if ((isBlank(theValueSetUrl) && isCodeSystemSupported(theValidationSupportContext, next, theCodeSystem)) + if ((isBlank(theValueSetUrl) + && canGenerateValidationResultForCodeSystem( + theValidationSupportContext, next, theCodeSystem)) || (isNotBlank(theValueSetUrl) && isValueSetSupported(theValidationSupportContext, next, theValueSetUrl))) { CodeValidationResult outcome = next.validateCode( @@ -1241,6 +1258,35 @@ public int hashCode() { } } + static class CanGenerateValidationResultForCodeSystemKey extends BaseKey { + + private final String myCodeSystemUrl; + private final IValidationSupport myValidationSupport; + private final int myHashCode; + + private CanGenerateValidationResultForCodeSystemKey( + IValidationSupport theValidationSupport, String theCodeSystemUrl) { + myValidationSupport = theValidationSupport; + myCodeSystemUrl = theCodeSystemUrl; + myHashCode = + Objects.hash("CanGenerateValidationResultForCodeSystemKey", theValidationSupport, myCodeSystemUrl); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof CanGenerateValidationResultForCodeSystemKey)) return false; + CanGenerateValidationResultForCodeSystemKey that = (CanGenerateValidationResultForCodeSystemKey) theO; + return myValidationSupport == that.myValidationSupport + && Objects.equals(myCodeSystemUrl, that.myCodeSystemUrl); + } + + @Override + public int hashCode() { + return myHashCode; + } + } + static class LookupCodeKey extends BaseKey { private final LookupCodeRequest myRequest; diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainTest.java index c04c9af3376e..1f9bf3b27911 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainTest.java @@ -185,25 +185,25 @@ public void testValidateCode_WithoutValueSetUrl(boolean theUseCache) { prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); - when(myValidationSupport0.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(false); - when(myValidationSupport1.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(true); + when(myValidationSupport0.canGenerateValidationResultForCodeSystem(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(false); + when(myValidationSupport1.canGenerateValidationResultForCodeSystem(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(true); when(myValidationSupport1.validateCode(any(), any(), any(), any(), any(), any())).thenAnswer(t -> new IValidationSupport.CodeValidationResult()); // Test IValidationSupport.CodeValidationResult result = chain.validateCode(newValidationCtx(chain), new ConceptValidationOptions(), CODE_SYSTEM_URL_0, CODE_0, DISPLAY_0, null); // Verify - verify(myValidationSupport0, times(1)).isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0)); - verify(myValidationSupport1, times(1)).isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0)); - verify(myValidationSupport2, never()).isCodeSystemSupported(any(), any()); + verify(myValidationSupport0, times(1)).canGenerateValidationResultForCodeSystem(any(), eq(CODE_SYSTEM_URL_0)); + verify(myValidationSupport1, times(1)).canGenerateValidationResultForCodeSystem(any(), eq(CODE_SYSTEM_URL_0)); + verify(myValidationSupport2, never()).canGenerateValidationResultForCodeSystem(any(), any()); verify(myValidationSupport0, never()).validateCode(any(), any(), any(), any(), any(), any()); verify(myValidationSupport1, times(1)).validateCode(any(), any(), any(), any(), any(), any()); verify(myValidationSupport2, never()).validateCode(any(), any(), any(), any(), any(), any()); // Setup for second execution (should use cache this time) prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); - when(myValidationSupport0.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(false); - when(myValidationSupport1.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(true); + when(myValidationSupport0.canGenerateValidationResultForCodeSystem(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(false); + when(myValidationSupport1.canGenerateValidationResultForCodeSystem(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(true); when(myValidationSupport1.validateCode(any(), any(), any(), any(), any(), any())).thenAnswer(t -> new IValidationSupport.CodeValidationResult()); // Test again (should use cache) @@ -215,9 +215,9 @@ public void testValidateCode_WithoutValueSetUrl(boolean theUseCache) { verifyNoInteractions(myValidationSupport0, myValidationSupport1, myValidationSupport2); } else { assertNotSame(result, result2); - verify(myValidationSupport0, times(1)).isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0)); - verify(myValidationSupport1, times(1)).isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0)); - verify(myValidationSupport2, never()).isCodeSystemSupported(any(), any()); + verify(myValidationSupport0, times(1)).canGenerateValidationResultForCodeSystem(any(), eq(CODE_SYSTEM_URL_0)); + verify(myValidationSupport1, times(1)).canGenerateValidationResultForCodeSystem(any(), eq(CODE_SYSTEM_URL_0)); + verify(myValidationSupport2, never()).canGenerateValidationResultForCodeSystem(any(), any()); verify(myValidationSupport0, never()).validateCode(any(), any(), any(), any(), any(), any()); verify(myValidationSupport1, times(1)).validateCode(any(), any(), any(), any(), any(), any()); verify(myValidationSupport2, never()).validateCode(any(), any(), any(), any(), any(), any()); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java index 4c7d07495854..c618635e9599 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java @@ -220,8 +220,8 @@ public void testCodedAnswer() { q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.CHOICE).setOptions(new Reference("http://somevalueset")); when(myValSupport.fetchResource(eq(Questionnaire.class), eq(QUESTIONNAIRE_URL))).thenReturn(q); - when(myValSupport.isCodeSystemSupported(any(), eq("http://codesystems.com/system"))).thenReturn(true); - when(myValSupport.isCodeSystemSupported(any(), eq("http://codesystems.com/system2"))).thenReturn(true); + when(myValSupport.canGenerateValidationResultForCodeSystem(any(), eq("http://codesystems.com/system"))).thenReturn(true); + when(myValSupport.canGenerateValidationResultForCodeSystem(any(), eq("http://codesystems.com/system2"))).thenReturn(true); when(myValSupport.validateCodeInValueSet(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(ValueSet.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode("code0")); when(myValSupport.validateCodeInValueSet(any(), any(), eq("http://codesystems.com/system"), eq("code1"), any(), nullable(ValueSet.class))) @@ -1030,8 +1030,8 @@ public void testOpenchoiceAnswer() { item.setLinkId("link0").setRequired(true).setType(QuestionnaireItemType.OPENCHOICE).setOptions(new Reference("http://somevalueset")); when(myValSupport.fetchResource(eq(Questionnaire.class), eq(questionnaireRef))).thenReturn(q); - when(myValSupport.isCodeSystemSupported(any(), eq("http://codesystems.com/system"))).thenReturn(true); - when(myValSupport.isCodeSystemSupported(any(), eq("http://codesystems.com/system2"))).thenReturn(true); + when(myValSupport.canGenerateValidationResultForCodeSystem(any(), eq("http://codesystems.com/system"))).thenReturn(true); + when(myValSupport.canGenerateValidationResultForCodeSystem(any(), eq("http://codesystems.com/system2"))).thenReturn(true); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(String.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode("code0")); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code1"), any(), nullable(String.class))) diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java index 17c347740b44..677a8018492c 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java @@ -215,8 +215,8 @@ public void testCodedAnswer() { q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.CHOICE).setAnswerValueSet("http://somevalueset"); when(myValSupport.fetchResource(eq(Questionnaire.class), eq("http://example.com/Questionnaire/q1"))).thenReturn(q); - when(myValSupport.isCodeSystemSupported(any(), eq("http://codesystems.com/system"))).thenReturn(true); - when(myValSupport.isCodeSystemSupported(any(), eq("http://codesystems.com/system2"))).thenReturn(true); + when(myValSupport.canGenerateValidationResultForCodeSystem(any(), eq("http://codesystems.com/system"))).thenReturn(true); + when(myValSupport.canGenerateValidationResultForCodeSystem(any(), eq("http://codesystems.com/system2"))).thenReturn(true); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(String.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode("code0")); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code1"), any(), nullable(String.class))) diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java index fbd25fbad332..ecff509e6116 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java @@ -215,8 +215,8 @@ public void testCodedAnswer() { q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.CODING).setAnswerValueSet("http://somevalueset"); when(myValSupport.fetchResource(eq(Questionnaire.class), eq("http://example.com/Questionnaire/q1"))).thenReturn(q); - when(myValSupport.isCodeSystemSupported(any(), eq("http://codesystems.com/system"))).thenReturn(true); - when(myValSupport.isCodeSystemSupported(any(), eq("http://codesystems.com/system2"))).thenReturn(true); + when(myValSupport.canGenerateValidationResultForCodeSystem(any(), eq("http://codesystems.com/system"))).thenReturn(true); + when(myValSupport.canGenerateValidationResultForCodeSystem(any(), eq("http://codesystems.com/system2"))).thenReturn(true); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(String.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode("code0")); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code1"), any(), nullable(String.class)))