Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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. <code>"http://loinc.org"</code>
* @return Returns <code>true</code> 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 <code>true</code> if a Remote Terminology Service is currently configured
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public InMemoryTerminologyServerValidationSupport inMemoryTerminologyServerValid
InMemoryTerminologyServerValidationSupport retVal =
new InMemoryTerminologyServerValidationSupport(theFhirContext);
retVal.setIssueSeverityForCodeDisplayMismatch(theStorageSettings.getIssueSeverityForCodeDisplayMismatch());
retVal.setIssueSeverityForUnknownCodeSystem(theStorageSettings.getIssueSeverityForUnknownCodeSystem());
return retVal;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ public void testValidateCodeOperation() {
.map(t -> ((IPrimitiveType<String>) 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'");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,22 +148,27 @@ public void testCanonicalization() {

private void assertHasInvalidCodeError(OperationOutcome theOperationOutcome) {
List<OperationOutcomeIssueComponent> 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<String, String> 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'))";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -216,6 +215,7 @@ public void validateCodeOperationOnValueSet_byUrlSystemAndCode() {
@Test
public void validateCodeOperationOnValueSet_byCodingAndUrlWhereValueSetIsUnknown_returnsFalse() {
myValueSetProvider.setShouldThrowExceptionForResourceNotFound(false);
myCodeSystemProvider.setShouldThrowExceptionForResourceNotFound(false);

Parameters respParam = myClient
.operation()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p/>
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -1102,18 +1132,22 @@ private Function<String, CodeSystem> 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -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;
}
Expand All @@ -106,7 +92,7 @@ public CodeValidationResult validateCodeInValueSet(
String theCode,
String theDisplay,
@Nonnull IBaseResource theValueSet) {
if (!canValidateCodeSystem(theValidationSupportContext, theCodeSystem)) {
if (!canGenerateValidationResultForCodeSystem(theValidationSupportContext, theCodeSystem)) {
return null;
}

Expand All @@ -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;
}
Expand Down
Loading
Loading