Make display name validation configurable (#5321)

* Test fixes

* Build cleanup

* Initial test passing

* Test fixes

* Tests all seem to be working

* Make display validation level configurable

* Should be all working

* Add changelog

* Add to changelog

---------

Co-authored-by: Tadgh <garygrantgraham@gmail.com>
This commit is contained in:
James Agnew 2023-09-19 18:25:33 -04:00 committed by GitHub
parent 50d2eac86f
commit 2b7ebbcb27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 558 additions and 176 deletions

View File

@ -553,11 +553,29 @@ public interface IValidationSupport {
private String myCodeSystemVersion;
private List<BaseConceptProperty> myProperties;
private String myDisplay;
private String mySourceDetails;
public CodeValidationResult() {
super();
}
/**
* This field may contain information about what the source of the
* validation information was.
*/
public String getSourceDetails() {
return mySourceDetails;
}
/**
* This field may contain information about what the source of the
* validation information was.
*/
public CodeValidationResult setSourceDetails(String theSourceDetails) {
mySourceDetails = theSourceDetails;
return this;
}
public String getDisplay() {
return myDisplay;
}

View File

@ -1,4 +1,7 @@
org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport.displayMismatch=Concept Display "{0}" does not match expected "{1}"
ca.uhn.fhir.jpa.term.TermReadSvcImpl.expansionRefersToUnknownCs=Unknown CodeSystem URI "{0}" referenced from ValueSet
ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetNotYetExpanded=ValueSet "{0}" has not yet been pre-expanded. Performing in-memory expansion without parameters. Current status: {1} | {2}
ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetNotYetExpanded_OffsetNotAllowed=ValueSet expansion can not combine "offset" with "ValueSet.compose.exclude" unless the ValueSet has been pre-expanded. ValueSet "{0}" must be pre-expanded for this operation to work.

View File

@ -0,0 +1,8 @@
---
type: add
issue: 5321
title: "It is now possible to configure the strictness of concept display name validation
using a new flag on the InMemoryTerminologyServerValidationSupport (for non-JPA validation)
and JpaStorageSettings (for JPA validation). In addition, the error messages emitted by
the validator when a concept display doesn't match have been improved to be much
more useful."

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.jpa.api.svc.IDeleteExpungeSvc;

View File

@ -23,6 +23,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.JpaPersistedResourceValidationSupport;
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain;
@ -30,6 +31,7 @@ import ca.uhn.fhir.jpa.validation.ValidatorPolicyAdvisor;
import ca.uhn.fhir.jpa.validation.ValidatorResourceFetcher;
import ca.uhn.fhir.validation.IInstanceValidatorModule;
import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport;
import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport;
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
import org.hl7.fhir.common.hapi.validation.validator.HapiToHl7OrgDstu2ValidatingSupportWrapper;
@ -45,6 +47,15 @@ public class ValidationSupportConfig {
return new DefaultProfileValidationSupport(theFhirContext);
}
@Bean
public InMemoryTerminologyServerValidationSupport inMemoryTerminologyServerValidationSupport(
FhirContext theFhirContext, JpaStorageSettings theStorageSettings) {
InMemoryTerminologyServerValidationSupport retVal =
new InMemoryTerminologyServerValidationSupport(theFhirContext);
retVal.setIssueSeverityForCodeDisplayMismatch(theStorageSettings.getIssueSeverityForCodeDisplayMismatch());
return retVal;
}
@Bean(name = JpaConfig.JPA_VALIDATION_SUPPORT_CHAIN)
public JpaValidationSupportChain jpaValidationSupportChain(FhirContext theFhirContext) {
return new JpaValidationSupportChain(theFhirContext);

View File

@ -61,6 +61,10 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class ValueSetOperationProvider extends BaseJpaProvider {
private static final Logger ourLog = LoggerFactory.getLogger(ValueSetOperationProvider.class);
public static final String SOURCE_DETAILS = "sourceDetails";
public static final String RESULT = "result";
public static final String MESSAGE = "message";
public static final String DISPLAY = "display";
@Autowired
protected IValidationSupport myValidationSupport;
@ -145,9 +149,10 @@ public class ValueSetOperationProvider extends BaseJpaProvider {
idempotent = true,
typeName = "ValueSet",
returnParameters = {
@OperationParam(name = "result", typeName = "boolean", min = 1),
@OperationParam(name = "message", typeName = "string"),
@OperationParam(name = "display", typeName = "string")
@OperationParam(name = RESULT, typeName = "boolean", min = 1),
@OperationParam(name = MESSAGE, typeName = "string"),
@OperationParam(name = DISPLAY, typeName = "string"),
@OperationParam(name = SOURCE_DETAILS, typeName = "string")
})
public IBaseParameters validateCode(
HttpServletRequest theServletRequest,
@ -159,7 +164,7 @@ public class ValueSetOperationProvider extends BaseJpaProvider {
@OperationParam(name = "system", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theSystem,
@OperationParam(name = "systemVersion", min = 0, max = 1, typeName = "string")
IPrimitiveType<String> theSystemVersion,
@OperationParam(name = "display", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theDisplay,
@OperationParam(name = DISPLAY, min = 0, max = 1, typeName = "string") IPrimitiveType<String> theDisplay,
@OperationParam(name = "coding", min = 0, max = 1, typeName = "Coding") IBaseCoding theCoding,
@OperationParam(name = "codeableConcept", min = 0, max = 1, typeName = "CodeableConcept")
ICompositeType theCodeableConcept,
@ -251,7 +256,7 @@ public class ValueSetOperationProvider extends BaseJpaProvider {
name = ProviderConstants.OPERATION_INVALIDATE_EXPANSION,
idempotent = false,
typeName = "ValueSet",
returnParameters = {@OperationParam(name = "message", typeName = "string", min = 1, max = 1)})
returnParameters = {@OperationParam(name = MESSAGE, typeName = "string", min = 1, max = 1)})
public IBaseParameters invalidateValueSetExpansion(
@IdParam IIdType theValueSetId, RequestDetails theRequestDetails, HttpServletRequest theServletRequest) {
startRequest(theServletRequest);
@ -260,7 +265,7 @@ public class ValueSetOperationProvider extends BaseJpaProvider {
String outcome = myTermReadSvc.invalidatePreCalculatedExpansion(theValueSetId, theRequestDetails);
IBaseParameters retVal = ParametersUtil.newInstance(getContext());
ParametersUtil.addParameterToParametersString(getContext(), retVal, "message", outcome);
ParametersUtil.addParameterToParametersString(getContext(), retVal, MESSAGE, outcome);
return retVal;
} finally {
@ -325,12 +330,16 @@ public class ValueSetOperationProvider extends BaseJpaProvider {
public static IBaseParameters toValidateCodeResult(FhirContext theContext, CodeValidationResult theResult) {
IBaseParameters retVal = ParametersUtil.newInstance(theContext);
ParametersUtil.addParameterToParametersBoolean(theContext, retVal, "result", theResult.isOk());
ParametersUtil.addParameterToParametersBoolean(theContext, retVal, RESULT, theResult.isOk());
if (isNotBlank(theResult.getMessage())) {
ParametersUtil.addParameterToParametersString(theContext, retVal, "message", theResult.getMessage());
ParametersUtil.addParameterToParametersString(theContext, retVal, MESSAGE, theResult.getMessage());
}
if (isNotBlank(theResult.getDisplay())) {
ParametersUtil.addParameterToParametersString(theContext, retVal, "display", theResult.getDisplay());
ParametersUtil.addParameterToParametersString(theContext, retVal, DISPLAY, theResult.getDisplay());
}
if (isNotBlank(theResult.getSourceDetails())) {
ParametersUtil.addParameterToParametersString(
theContext, retVal, SOURCE_DETAILS, theResult.getSourceDetails());
}
return retVal;

View File

@ -295,6 +295,9 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
@Autowired
private IJpaStorageResourceParser myJpaStorageResourceParser;
@Autowired
private InMemoryTerminologyServerValidationSupport myInMemoryTerminologyServerValidationSupport;
@Override
public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
TermCodeSystemVersionDetails cs = getCurrentCodeSystemVersion(theSystem);
@ -1025,11 +1028,8 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
new VersionConvertor_40_50(new BaseAdvisor_40_50()), "ValueSet");
org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent includeOrExclude =
ValueSet40_50.convertConceptSetComponent(theIncludeOrExclude);
new InMemoryTerminologyServerValidationSupport(myContext)
.expandValueSetIncludeOrExclude(
new ValidationSupportContext(provideValidationSupport()),
consumer,
includeOrExclude);
myInMemoryTerminologyServerValidationSupport.expandValueSetIncludeOrExclude(
new ValidationSupportContext(provideValidationSupport()), consumer, includeOrExclude);
} catch (InMemoryTerminologyServerValidationSupport.ExpansionCouldNotBeCompletedInternallyException e) {
if (theExpansionOptions != null
&& !theExpansionOptions.isFailOnMissingCodeSystem()
@ -2055,7 +2055,7 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
.findByResourcePid(valueSetResourcePid.getId())
.orElseThrow(IllegalStateException::new);
String timingDescription = toHumanReadableExpansionTimestamp(valueSetEntity);
String msg = myContext
String preExpansionMessage = myContext
.getLocalizer()
.getMessage(TermReadSvcImpl.class, "validationPerformedAgainstPreExpansion", timingDescription);
@ -2068,14 +2068,18 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
.setCode(concept.getCode())
.setDisplay(concept.getDisplay())
.setCodeSystemVersion(concept.getSystemVersion())
.setMessage(msg);
.setSourceDetails(preExpansionMessage);
}
}
String expectedDisplay = concepts.get(0).getDisplay();
String append = createMessageAppendForDisplayMismatch(theSystem, theDisplay, expectedDisplay) + " - " + msg;
return createFailureCodeValidationResult(theSystem, theCode, systemVersion, append)
.setDisplay(expectedDisplay);
return InMemoryTerminologyServerValidationSupport.createResultForDisplayMismatch(
myContext,
theCode,
theDisplay,
expectedDisplay,
systemVersion,
myStorageSettings.getIssueSeverityForCodeDisplayMismatch());
}
if (!concepts.isEmpty()) {
@ -2083,7 +2087,7 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
.setCode(concepts.get(0).getCode())
.setDisplay(concepts.get(0).getDisplay())
.setCodeSystemVersion(concepts.get(0).getSystemVersion())
.setMessage(msg);
.setMessage(preExpansionMessage);
}
// Ok, we failed
@ -2096,7 +2100,7 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
String unknownCodeMessage = myContext
.getLocalizer()
.getMessage(TermReadSvcImpl.class, "unknownCodeInSystem", theSystem, theCode);
append = " - " + unknownCodeMessage + ". " + msg;
append = " - " + unknownCodeMessage + ". " + preExpansionMessage;
}
return createFailureCodeValidationResult(theSystem, theCode, null, append);
@ -2710,11 +2714,13 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
|| code.getDisplay().equals(theDisplay)) {
return new CodeValidationResult().setCode(code.getCode()).setDisplay(code.getDisplay());
} else {
String messageAppend =
createMessageAppendForDisplayMismatch(theCodeSystemUrl, theDisplay, code.getDisplay());
return createFailureCodeValidationResult(
theCodeSystemUrl, theCode, code.getSystemVersion(), messageAppend)
.setDisplay(code.getDisplay());
return InMemoryTerminologyServerValidationSupport.createResultForDisplayMismatch(
myContext,
theCode,
theDisplay,
code.getDisplay(),
code.getSystemVersion(),
myStorageSettings.getIssueSeverityForCodeDisplayMismatch());
}
}
@ -2752,14 +2758,13 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
if (retVal == null) {
if (valueSet != null) {
retVal = new InMemoryTerminologyServerValidationSupport(myContext)
.validateCodeInValueSet(
theValidationSupportContext,
theValidationOptions,
theCodeSystem,
theCode,
theDisplay,
valueSet);
retVal = myInMemoryTerminologyServerValidationSupport.validateCodeInValueSet(
theValidationSupportContext,
theValidationOptions,
theCodeSystem,
theCode,
theDisplay,
valueSet);
} else {
String append = " - Unable to locate ValueSet[" + theValueSetUrl + "]";
retVal = createFailureCodeValidationResult(theCodeSystem, theCode, null, append);
@ -3182,13 +3187,6 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
return theExpansionOptions.getTheDisplayLanguage().equalsIgnoreCase(theStoredLang);
}
@Nonnull
private static String createMessageAppendForDisplayMismatch(
String theCodeSystemUrl, String theDisplay, String theExpectedDisplay) {
return " - Concept Display \"" + theDisplay + "\" does not match expected \"" + theExpectedDisplay
+ "\" for CodeSystem: " + theCodeSystemUrl;
}
@Nonnull
private static String createMessageAppendForCodeNotFoundInCodeSystem(String theCodeSystemUrl) {
return " - Code is not found in CodeSystem: " + theCodeSystemUrl;

View File

@ -59,6 +59,9 @@ public class JpaValidationSupportChain extends ValidationSupportChain {
@Autowired
private UnknownCodeSystemWarningValidationSupport myUnknownCodeSystemWarningValidationSupport;
@Autowired
private InMemoryTerminologyServerValidationSupport myInMemoryTerminologyServerValidationSupport;
/**
* Constructor
*/
@ -82,7 +85,7 @@ public class JpaValidationSupportChain extends ValidationSupportChain {
addValidationSupport(myJpaValidationSupport);
addValidationSupport(myTerminologyService);
addValidationSupport(new SnapshotGeneratingValidationSupport(myFhirContext));
addValidationSupport(new InMemoryTerminologyServerValidationSupport(myFhirContext));
addValidationSupport(myInMemoryTerminologyServerValidationSupport);
addValidationSupport(myNpmJpaValidationSupport);
addValidationSupport(new CommonCodeSystemsTerminologyService(myFhirContext));
addValidationSupport(myConceptMappingSvc);

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.mdm.config;
import ca.uhn.fhir.context.FhirContext;

View File

@ -103,9 +103,10 @@ public class FhirResourceDaoValueSetDstu2Test extends BaseJpaDstu2Test {
CodingDt coding = null;
CodeableConceptDt codeableConcept = null;
IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd);
assertFalse(result.isOk());
assertTrue(result.isOk());
assertEquals("Concept Display \"Systolic blood pressure at First encounterXXXX\" does not match expected \"Systolic blood pressure at First encounter\" for in-memory expansion of ValueSet: http://www.healthintersections.com.au/fhir/ValueSet/extensional-case-2", result.getMessage());
assertEquals("Systolic blood pressure at First encounter", result.getDisplay());
assertEquals(IValidationSupport.IssueSeverity.WARNING, result.getSeverity());
}
@Test

View File

@ -226,7 +226,7 @@ public class FhirResourceDaoDstu3ValueSetTest extends BaseJpaDstu3Test {
Coding coding = null;
CodeableConcept codeableConcept = null;
IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd);
assertFalse(result.isOk());
assertTrue(result.isOk());
assertEquals("Systolic blood pressure at First encounter", result.getDisplay());
}

View File

@ -9,6 +9,7 @@ import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.provider.ValueSetOperationProvider;
import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -789,14 +790,14 @@ public class ResourceProviderDstu3ValueSetTest extends BaseResourceProviderDstu3
String resp = myFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam);
ourLog.info(resp);
assertEquals("result", respParam.getParameter().get(0).getName());
assertEquals(ValueSetOperationProvider.RESULT, respParam.getParameter().get(0).getName());
assertEquals(true, ((BooleanType) respParam.getParameter().get(0).getValue()).getValue());
assertEquals("message", respParam.getParameter().get(1).getName());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://hl7.org/fhir/ValueSet/administrative-gender", ((StringType) respParam.getParameter().get(1).getValue()).getValue());
assertEquals(ValueSetOperationProvider.DISPLAY, respParam.getParameter().get(1).getName());
assertEquals("Male", ((StringType) respParam.getParameter().get(1).getValue()).getValue());
assertEquals("display", respParam.getParameter().get(2).getName());
assertEquals("Male", ((StringType) respParam.getParameter().get(2).getValue()).getValue());
assertEquals(ValueSetOperationProvider.SOURCE_DETAILS, respParam.getParameter().get(2).getName());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://hl7.org/fhir/ValueSet/administrative-gender", ((StringType) respParam.getParameter().get(2).getValue()).getValue());
}
/**
@ -819,14 +820,15 @@ public class ResourceProviderDstu3ValueSetTest extends BaseResourceProviderDstu3
String resp = myFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam);
ourLog.info(resp);
assertEquals("result", respParam.getParameter().get(0).getName());
assertEquals(ValueSetOperationProvider.RESULT, respParam.getParameter().get(0).getName());
assertEquals(true, ((BooleanType) respParam.getParameter().get(0).getValue()).getValue());
assertEquals("message", respParam.getParameter().get(1).getName());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://hl7.org/fhir/ValueSet/administrative-gender", ((StringType) respParam.getParameter().get(1).getValue()).getValue());
assertEquals(ValueSetOperationProvider.DISPLAY, respParam.getParameter().get(1).getName());
assertEquals("Male", ((StringType) respParam.getParameter().get(1).getValue()).getValue());
assertEquals(ValueSetOperationProvider.SOURCE_DETAILS, respParam.getParameter().get(2).getName());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://hl7.org/fhir/ValueSet/administrative-gender", ((StringType) respParam.getParameter().get(2).getValue()).getValue());
assertEquals("display", respParam.getParameter().get(2).getName());
assertEquals("Male", ((StringType) respParam.getParameter().get(2).getValue()).getValue());
}
@Test

View File

@ -34,6 +34,7 @@ import ca.uhn.fhir.validation.IValidatorModule;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import ca.uhn.fhir.validation.ValidationResult;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport;
import org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValidationSupport;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
@ -49,6 +50,9 @@ import org.hl7.fhir.utilities.xhtml.XhtmlNode;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.util.AopTestUtils;
@ -85,15 +89,21 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
private ValidationSettings myValidationSettings;
@Autowired
private UnknownCodeSystemWarningValidationSupport myUnknownCodeSystemWarningValidationSupport;
@Autowired
private InMemoryTerminologyServerValidationSupport myInMemoryTerminologyServerValidationSupport;
@AfterEach
public void after() {
FhirInstanceValidator val = AopTestUtils.getTargetObject(myValidatorModule);
val.setBestPracticeWarningLevel(BestPracticeWarningLevel.Warning);
myStorageSettings.setAllowExternalReferences(new JpaStorageSettings().isAllowExternalReferences());
myStorageSettings.setMaximumExpansionSize(JpaStorageSettings.DEFAULT_MAX_EXPANSION_SIZE);
myStorageSettings.setPreExpandValueSets(new JpaStorageSettings().isPreExpandValueSets());
JpaStorageSettings defaults = new JpaStorageSettings();
myStorageSettings.setAllowExternalReferences(defaults.isAllowExternalReferences());
myStorageSettings.setMaximumExpansionSize(defaults.getMaximumExpansionSize());
myStorageSettings.setPreExpandValueSets(defaults.isPreExpandValueSets());
myStorageSettings.setIssueSeverityForCodeDisplayMismatch(defaults.getIssueSeverityForCodeDisplayMismatch());
myInMemoryTerminologyServerValidationSupport.setIssueSeverityForCodeDisplayMismatch(defaults.getIssueSeverityForCodeDisplayMismatch());
TermReadSvcImpl.setInvokeOnNextCallForUnitTest(null);
@ -125,7 +135,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
ourLog.info(encoded);
assertEquals(1, oo.getIssue().size(), encoded);
assertThat(oo.getIssue().get(0).getDiagnostics(),
containsString("The code provided (http://cs#code99) is not in the value set"));
containsString("provided (http://cs#code99) is not in the value set"));
assertThat(oo.getIssue().get(0).getDiagnostics(),
containsString("Unknown code 'http://cs#code99' for in-memory expansion of ValueSet 'http://vs'"));
assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssueFirstRep().getSeverity(), encoded);
@ -159,7 +169,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
ourLog.info(encoded);
assertEquals(1, oo.getIssue().size());
assertThat(oo.getIssueFirstRep().getDiagnostics(),
containsString("The code provided (http://cs#code99) is not in the value set"));
containsString("provided (http://cs#code99) is not in the value set"));
assertThat(oo.getIssueFirstRep().getDiagnostics(),
containsString("Unknown code 'http://cs#code99' for in-memory expansion of ValueSet 'http://vs'"));
assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssueFirstRep().getSeverity());
@ -199,7 +209,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
containsString("CodeSystem is unknown and can't be validated: http://cs for 'http://cs#code99'"));
assertEquals(OperationOutcome.IssueSeverity.WARNING, oo.getIssue().get(0).getSeverity());
assertThat(oo.getIssue().get(1).getDiagnostics(),
containsString("The code provided (http://cs#code99) is not in the value set 'ValueSet[http://vs]'"));
containsString("provided (http://cs#code99) is not in the value set 'ValueSet[http://vs]'"));
assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssue().get(1).getSeverity());
}
@ -239,7 +249,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
ourLog.info(encoded);
assertEquals(1, oo.getIssue().size());
assertThat(oo.getIssue().get(0).getDiagnostics(),
containsString("The code provided (http://cs#code99) is not in the value set"));
containsString("provided (http://cs#code99) is not in the value set"));
assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssueFirstRep().getSeverity());
}
@ -335,7 +345,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
ourLog.info(encoded);
assertEquals(1, oo.getIssue().size());
assertThat(oo.getIssue().get(0).getDiagnostics(),
containsString("The code provided (http://cs#code1) is not in the value set"));
containsString("provided (http://cs#code1) is not in the value set"));
assertThat(oo.getIssue().get(0).getDiagnostics(),
containsString("Failed to expand ValueSet 'http://vs' (in-memory). Could not validate code http://cs#code1"));
assertThat(oo.getIssue().get(0).getDiagnostics(),
@ -343,7 +353,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssueFirstRep().getSeverity());
assertEquals(27, ((IntegerType)oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-line").getValue()).getValue());
assertEquals(4, ((IntegerType)oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-col").getValue()).getValue());
assertEquals("Terminology_TX_Confirm_4a", ((StringType)oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id").getValue()).getValue());
assertEquals("Terminology_TX_NoValid_12", ((StringType)oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id").getValue()).getValue());
assertEquals(OperationOutcome.IssueType.PROCESSING, oo.getIssue().get(0).getCode());
assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssue().get(0).getSeverity());
assertEquals(2, oo.getIssue().get(0).getLocation().size());
@ -509,7 +519,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
String outcomeStr = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
ourLog.info("Validation outcome: {}", outcomeStr);
assertThat(outcomeStr,
containsString("The code provided (http://unitsofmeasure.org#cm) is not in the value set"));
containsString("provided (http://unitsofmeasure.org#cm) is not in the value set"));
// Before, the VS wasn't pre-expanded. Try again with it pre-expanded
runInTransaction(() -> {
@ -538,7 +548,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
outcomeStr = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
ourLog.info("Validation outcome: {}", outcomeStr);
assertThat(outcomeStr,
containsString("The code provided (http://unitsofmeasure.org#cm) is not in the value set"));
containsString("provided (http://unitsofmeasure.org#cm) is not in the value set"));
}
@ -1364,7 +1374,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
}
@Test
public void testValidateUsingExternallyDefinedCodeMisMatchDisplay_ShouldError() {
public void testValidateUsingExternallyDefinedCodeMisMatchDisplay_InMemory_ShouldLogWarning() {
CodeSystem codeSystem = new CodeSystem();
codeSystem.setUrl("http://foo");
codeSystem.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT);
@ -1396,8 +1406,8 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
containsString("None of the codings provided are in the value set 'IdentifierType'"));
assertThat(OperationOutcomeUtil.getFirstIssueDetails(myFhirContext, oo),
containsString("a coding should come from this value set unless it has no suitable code (note that the validator cannot judge what is suitable) (codes = http://foo#bar)"));
assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssue().get(1).getSeverity());
assertThat(oo.getIssue().get(1).getDiagnostics(), containsString("Unable to validate code http://foo#bar - Concept Display "));
assertEquals(OperationOutcome.IssueSeverity.WARNING, oo.getIssue().get(1).getSeverity());
assertEquals("Concept Display \"not bar code\" does not match expected \"Bar Code\" for 'http://foo#bar'", oo.getIssue().get(1).getDiagnostics());
}
private OperationOutcome doTestValidateResourceContainingProfileDeclaration(String methodName, EncodingEnum enc) throws IOException {
@ -1994,6 +2004,105 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
assertThat(encoded, containsString("No issues detected"));
}
@ParameterizedTest
@CsvSource(value = {
"INFORMATION, false",
"INFORMATION, true",
"WARNING, false",
"WARNING, true",
"ERROR, false",
"ERROR, true",
})
public void testValidateWrongDisplayOnRequiredBinding(IValidationSupport.IssueSeverity theDisplayCodeMismatchIssueSeverity, boolean thePreCalculateExpansion) {
myStorageSettings.setIssueSeverityForCodeDisplayMismatch(theDisplayCodeMismatchIssueSeverity);
myInMemoryTerminologyServerValidationSupport.setIssueSeverityForCodeDisplayMismatch(theDisplayCodeMismatchIssueSeverity);
StructureDefinition sd = new StructureDefinition();
sd.setUrl("http://profile");
sd.setStatus(Enumerations.PublicationStatus.ACTIVE);
sd.setType("Observation");
sd.setAbstract(false);
sd.setDerivation(StructureDefinition.TypeDerivationRule.CONSTRAINT);
sd.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/Observation");
ElementDefinition codeElement = sd.getDifferential().addElement();
codeElement.setId("Observation.code");
codeElement.setPath("Observation.code");
codeElement.addType().setCode("CodeableConcept");
codeElement.getBinding().setStrength(Enumerations.BindingStrength.REQUIRED);
codeElement.getBinding().setValueSet("http://vs");
myStructureDefinitionDao.create(sd, new SystemRequestDetails());
CodeSystem cs = new CodeSystem();
cs.setUrl("http://cs");
cs.setStatus(Enumerations.PublicationStatus.ACTIVE);
cs.setContent(CodeSystem.CodeSystemContentMode.COMPLETE);
cs.addConcept()
.setCode("8302-2")
.setDisplay("Body Height");
myCodeSystemDao.create(cs, new SystemRequestDetails());
ValueSet vs = new ValueSet();
vs.setUrl("http://vs");
vs.setStatus(Enumerations.PublicationStatus.ACTIVE);
vs.getCompose().addInclude().setSystem("http://cs");
myValueSetDao.create(vs, new SystemRequestDetails());
if (thePreCalculateExpansion) {
myTermReadSvc.preExpandDeferredValueSetsToTerminologyTables();
}
Observation obs = new Observation();
obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED);
obs.getText().setDivAsString("<div>hello</div>");
obs.getMeta().addProfile("http://profile");
obs.setStatus(Observation.ObservationStatus.FINAL);
obs.getCode().addCoding()
.setSystem("http://cs")
.setCode("8302-2")
.setDisplay("Body height2");
obs.setEffective(DateTimeType.now());
obs.addPerformer(new Reference("Practitioner/123"));
obs.setSubject(new Reference("Patient/123"));
obs.setValue(new Quantity(null, 123, "http://unitsofmeasure.org", "[in_i]", "in"));
String encoded = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs);
MethodOutcome outcome = myObservationDao.validate(obs, null, encoded, EncodingEnum.JSON, ValidationModeEnum.CREATE, null, new SystemRequestDetails());
OperationOutcome oo = (OperationOutcome) outcome.getOperationOutcome();
ourLog.info("Outcome: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo).replace("\"resourceType\"", "\"resType\""));
OperationOutcome.OperationOutcomeIssueComponent badDisplayIssue;
if (theDisplayCodeMismatchIssueSeverity == IValidationSupport.IssueSeverity.ERROR) {
assertEquals(2, oo.getIssue().size());
badDisplayIssue = oo.getIssue().get(1);
OperationOutcome.OperationOutcomeIssueComponent noGoodCodings = oo.getIssue().get(0);
assertEquals("error", noGoodCodings.getSeverity().toCode());
assertEquals("None of the codings provided are in the value set 'ValueSet[http://vs]' (http://vs), and a coding from this value set is required) (codes = http://cs#8302-2)", noGoodCodings.getDiagnostics());
} else if (theDisplayCodeMismatchIssueSeverity == IValidationSupport.IssueSeverity.WARNING) {
assertEquals(1, oo.getIssue().size());
badDisplayIssue = oo.getIssue().get(0);
assertThat(badDisplayIssue.getDiagnostics(),
containsString("Concept Display \"Body height2\" does not match expected \"Body Height\""));
assertEquals(OperationOutcome.IssueType.PROCESSING, badDisplayIssue.getCode());
assertEquals(theDisplayCodeMismatchIssueSeverity.name().toLowerCase(), badDisplayIssue.getSeverity().toCode());
} else {
assertEquals(1, oo.getIssue().size());
badDisplayIssue = oo.getIssue().get(0);
assertThat(badDisplayIssue.getDiagnostics(),
containsString("No issues detected during validation"));
assertEquals(OperationOutcome.IssueType.INFORMATIONAL, badDisplayIssue.getCode());
assertEquals(theDisplayCodeMismatchIssueSeverity.name().toLowerCase(), badDisplayIssue.getSeverity().toCode());
}
}
/**
* See #1780
*/

View File

@ -148,7 +148,7 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test {
outcome = myValidationSupport.validateCode(ctx, options, "http://cs", "child10", null, "http://vs");
assertNotNull(outcome);
assertTrue(outcome.isOk());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://vs", outcome.getMessage());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://vs", outcome.getSourceDetails());
outcome = myValidationSupport.validateCode(ctx, options, "http://cs", "childX", null, "http://vs");
assertNotNull(outcome);
@ -160,7 +160,7 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test {
outcome = myValidationSupport.validateCode(ctx, options, "http://cs-np", "code1", null, "http://vs");
assertNotNull(outcome);
assertTrue(outcome.isOk());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://vs", outcome.getMessage());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://vs", outcome.getSourceDetails());
outcome = myValidationSupport.validateCode(ctx, options, "http://cs-np", "codeX", null, "http://vs");
assertNotNull(outcome);
@ -251,7 +251,7 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test {
outcome = myValidationSupport.validateCode(ctx, options, "http://cs", "child10", null, "http://vs");
assertNotNull(outcome);
assertTrue(outcome.isOk());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://vs", outcome.getMessage());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://vs", outcome.getSourceDetails());
outcome = myValidationSupport.validateCode(ctx, options, "http://cs", "childX", null, "http://vs");
assertNotNull(outcome);
@ -263,7 +263,7 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test {
outcome = myValidationSupport.validateCode(ctx, options, "http://cs-np", "code1", null, "http://vs");
assertNotNull(outcome);
assertTrue(outcome.isOk());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://vs", outcome.getMessage());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://vs", outcome.getSourceDetails());
outcome = myValidationSupport.validateCode(ctx, options, "http://cs-np", "codeX", null, "http://vs");
assertNotNull(outcome);
@ -344,7 +344,7 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test {
Coding coding = null;
CodeableConcept codeableConcept = null;
IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd);
assertFalse(result.isOk());
assertTrue(result.isOk());
assertEquals("Systolic blood pressure at First encounter", result.getDisplay());
assertEquals("Concept Display \"Systolic blood pressure at First encounterXXXX\" does not match expected \"Systolic blood pressure at First encounter\" for in-memory expansion of ValueSet: http://www.healthintersections.com.au/fhir/ValueSet/extensional-case-2", result.getMessage());
}

View File

@ -529,8 +529,8 @@ public class ResourceProviderR4CodeSystemTest extends BaseResourceProviderR4Test
String resp = myFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam);
ourLog.info(resp);
assertFalse(((BooleanType) respParam.getParameter().get(0).getValue()).booleanValue());
assertEquals("Unable to validate code http://acme.org#8452-5 - Concept Display \"Old Systolic blood pressure.inspiration - expiration\" does not match expected \"Systolic blood pressure.inspiration - expiration\" for CodeSystem: http://acme.org", ((StringType) respParam.getParameter().get(1).getValue()).getValueAsString());
assertTrue(((BooleanType) respParam.getParameter().get(0).getValue()).booleanValue());
assertEquals("Concept Display \"Old Systolic blood pressure.inspiration - expiration\" does not match expected \"Systolic blood pressure.inspiration - expiration\"", ((StringType) respParam.getParameter().get(1).getValue()).getValueAsString());
}
@Test

View File

@ -14,6 +14,7 @@ import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test;
import ca.uhn.fhir.jpa.provider.ValueSetOperationProvider;
import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
@ -1294,14 +1295,14 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv
String resp = myFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam);
ourLog.info(resp);
assertEquals("result", respParam.getParameter().get(0).getName());
assertEquals(ValueSetOperationProvider.RESULT, respParam.getParameter().get(0).getName());
assertEquals(true, ((BooleanType) respParam.getParameter().get(0).getValue()).getValue());
assertEquals("message", respParam.getParameter().get(1).getName());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://hl7.org/fhir/ValueSet/administrative-gender", ((StringType) respParam.getParameter().get(1).getValue()).getValue());
assertEquals(ValueSetOperationProvider.DISPLAY, respParam.getParameter().get(1).getName());
assertEquals("Male", ((StringType) respParam.getParameter().get(1).getValue()).getValue());
assertEquals("display", respParam.getParameter().get(2).getName());
assertEquals("Male", ((StringType) respParam.getParameter().get(2).getValue()).getValue());
assertEquals(ValueSetOperationProvider.SOURCE_DETAILS, respParam.getParameter().get(2).getName());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://hl7.org/fhir/ValueSet/administrative-gender", ((StringType) respParam.getParameter().get(2).getValue()).getValue());
}
@Test

View File

@ -30,8 +30,6 @@ import ca.uhn.fhir.jpa.esr.ExternallyStoredResourceServiceRegistry;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorR4;
@ -62,7 +60,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.quartz.JobKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;

View File

@ -466,7 +466,7 @@ public class ValueSetExpansionR4Test extends BaseTermR4Test {
String code = "male";
IValidationSupport.CodeValidationResult outcome = myValueSetDao.validateCode(new CodeType(valueSetUrl), null, new CodeType(code), new CodeType(codeSystemUrl), null, null, null, mySrd);
assertTrue(outcome.isOk());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://hl7.org/fhir/ValueSet/administrative-gender", outcome.getMessage());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://hl7.org/fhir/ValueSet/administrative-gender", outcome.getSourceDetails());
// Validate Code - Bad
code = "AAA";
@ -1635,10 +1635,11 @@ public class ValueSetExpansionR4Test extends BaseTermR4Test {
code = "28571000087109";
display = "BLAH";
outcome = myValueSetDao.validateCode(null, vsId, new CodeType(code), new UriType(codeSystemUrl), new StringType(display), null, null, mySrd);
assertFalse(outcome.isOk());
assertEquals(null, outcome.getCode());
assertTrue(outcome.isOk());
assertEquals("28571000087109", outcome.getCode());
assertEquals("MODERNA COVID-19 mRNA-1273", outcome.getDisplay());
assertEquals("Concept Display \"BLAH\" does not match expected \"MODERNA COVID-19 mRNA-1273\" for in-memory expansion of ValueSet: http://ehealthontario.ca/fhir/ValueSet/vaccinecode", outcome.getMessage());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://ehealthontario.ca/fhir/ValueSet/vaccinecode", outcome.getSourceDetails());
assertEquals("0.17", outcome.getCodeSystemVersion());
// Validate code - good code, good display
@ -1680,11 +1681,11 @@ public class ValueSetExpansionR4Test extends BaseTermR4Test {
code = "28571000087109";
display = "BLAH";
outcome = myValueSetDao.validateCode(null, vsId, new CodeType(code), new UriType(codeSystemUrl), new StringType(display), null, null, mySrd);
assertFalse(outcome.isOk());
assertEquals(null, outcome.getCode());
assertTrue(outcome.isOk());
assertEquals("28571000087109", outcome.getCode());
assertEquals("MODERNA COVID-19 mRNA-1273", outcome.getDisplay());
assertEquals("0.17", outcome.getCodeSystemVersion());
assertThat(outcome.getMessage(), containsString("Unable to validate code http://snomed.info/sct#28571000087109 - Concept Display \"BLAH\" does not match expected \"MODERNA COVID-19 mRNA-1273\" for CodeSystem: http://snomed.info/sct - Code validation occurred using a ValueSet expansion that was pre-calculated at"));
assertEquals("Concept Display \"BLAH\" does not match expected \"MODERNA COVID-19 mRNA-1273\"", outcome.getMessage());
// Validate code - good code, good display
codeSystemUrl = "http://snomed.info/sct";

View File

@ -117,7 +117,7 @@ public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test {
Coding coding = null;
CodeableConcept codeableConcept = null;
IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd);
assertFalse(result.isOk());
assertTrue(result.isOk());
assertEquals("Systolic blood pressure at First encounter", result.getDisplay());
assertEquals("Concept Display \"Systolic blood pressure at First encounterXXXX\" does not match expected \"Systolic blood pressure at First encounter\" for in-memory expansion of ValueSet: http://www.healthintersections.com.au/fhir/ValueSet/extensional-case-2", result.getMessage());
}

View File

@ -12,6 +12,7 @@ import ca.uhn.fhir.jpa.entity.TermValueSetConcept;
import ca.uhn.fhir.jpa.entity.TermValueSetPreExpansionStatusEnum;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.provider.ValueSetOperationProvider;
import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -1228,14 +1229,14 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test {
String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam);
ourLog.info(resp);
assertEquals("result", respParam.getParameter().get(0).getName());
assertEquals(ValueSetOperationProvider.RESULT, respParam.getParameter().get(0).getName());
assertEquals(true, ((BooleanType) respParam.getParameter().get(0).getValue()).getValue());
assertEquals("message", respParam.getParameter().get(1).getName());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://hl7.org/fhir/ValueSet/administrative-gender", ((StringType) respParam.getParameter().get(1).getValue()).getValue());
assertEquals(ValueSetOperationProvider.DISPLAY, respParam.getParameter().get(1).getName());
assertEquals("Male", ((StringType) respParam.getParameter().get(1).getValue()).getValue());
assertEquals("display", respParam.getParameter().get(2).getName());
assertEquals("Male", ((StringType) respParam.getParameter().get(2).getValue()).getValue());
assertEquals(ValueSetOperationProvider.SOURCE_DETAILS, respParam.getParameter().get(2).getName());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://hl7.org/fhir/ValueSet/administrative-gender", ((StringType) respParam.getParameter().get(2).getValue()).getValue());
}
// Good code and system, but not in specified valueset

View File

@ -1,6 +1,6 @@
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* HAPI FHIR - Master Data Management
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%

View File

@ -42,7 +42,8 @@ public class MailSvcIT {
// execute
fixture.sendMail(email);
// validate
assertTrue(ourGreenMail.waitForIncomingEmail(5000, 1));
boolean condition = ourGreenMail.waitForIncomingEmail(5000, 1);
assertTrue(condition);
final MimeMessage[] receivedMessages = ourGreenMail.getReceivedMessages();
assertEquals(1, receivedMessages.length);
assertEquals(SUBJECT, receivedMessages[0].getSubject());

View File

@ -19,6 +19,7 @@
*/
package ca.uhn.fhir.jpa.api.config;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.api.model.HistoryCountModeEnum;
import ca.uhn.fhir.jpa.api.model.WarmCacheEntry;
import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
@ -334,6 +335,13 @@ public class JpaStorageSettings extends StorageSettings {
*/
private boolean myResourceHistoryDbEnabled = true;
/**
* @since 7.0.0
*/
@Nonnull
private IValidationSupport.IssueSeverity myIssueSeverityForCodeDisplayMismatch =
IValidationSupport.IssueSeverity.WARNING;
/**
* This setting allows preventing a conditional update to invalidate the match criteria.
* <p/>
@ -2376,6 +2384,41 @@ public class JpaStorageSettings extends StorageSettings {
return myNonResourceDbHistoryEnabled;
}
/**
* This setting controls the validation issue severity to report when a code validation
* finds that the code is present in the given CodeSystem, but the display name being
* validated doesn't match the expected value(s). Defaults to
* {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#WARNING}. Set this
* value to {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#INFORMATION}
* if you don't want to see display name validation issues at all in resource validation
* outcomes.
*
* @since 7.0.0
*/
@Nonnull
public IValidationSupport.IssueSeverity getIssueSeverityForCodeDisplayMismatch() {
return myIssueSeverityForCodeDisplayMismatch;
}
/**
* This setting controls the validation issue severity to report when a code validation
* finds that the code is present in the given CodeSystem, but the display name being
* validated doesn't match the expected value(s). Defaults to
* {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#WARNING}. Set this
* value to {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#INFORMATION}
* if you don't want to see display name validation issues at all in resource validation
* outcomes.
*
* @param theIssueSeverityForCodeDisplayMismatch The severity. Must not be {@literal null}.
* @since 7.0.0
*/
public void setIssueSeverityForCodeDisplayMismatch(
@Nonnull IValidationSupport.IssueSeverity theIssueSeverityForCodeDisplayMismatch) {
Validate.notNull(
theIssueSeverityForCodeDisplayMismatch, "theIssueSeverityForCodeDisplayMismatch must not be null");
myIssueSeverityForCodeDisplayMismatch = theIssueSeverityForCodeDisplayMismatch;
}
/**
* This setting controls whether MdmLink and other non-resource DB history is enabled.
* <p/>

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Test Utilities
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.models;
import org.springframework.core.io.AbstractResource;

View File

@ -57,16 +57,54 @@ import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTermi
@SuppressWarnings("EnhancedSwitchMigration")
public class InMemoryTerminologyServerValidationSupport implements IValidationSupport {
private static final String OUR_PIPE_CHARACTER = "|";
private final FhirContext myCtx;
private final VersionCanonicalizer myVersionCanonicalizer;
private IssueSeverity myIssueSeverityForCodeDisplayMismatch = IssueSeverity.WARNING;
/**
* Constructor
*
* @param theCtx A FhirContext for the FHIR version being validated
*/
public InMemoryTerminologyServerValidationSupport(FhirContext theCtx) {
Validate.notNull(theCtx, "theCtx must not be null");
myCtx = theCtx;
myVersionCanonicalizer = new VersionCanonicalizer(theCtx);
}
/**
* This setting controls the validation issue severity to report when a code validation
* finds that the code is present in the given CodeSystem, but the display name being
* validated doesn't match the expected value(s). Defaults to
* {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#WARNING}. Set this
* value to {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#INFORMATION}
* if you don't want to see display name validation issues at all in resource validation
* outcomes.
*
* @since 7.0.0
*/
public IssueSeverity getIssueSeverityForCodeDisplayMismatch() {
return myIssueSeverityForCodeDisplayMismatch;
}
/**
* This setting controls the validation issue severity to report when a code validation
* finds that the code is present in the given CodeSystem, but the display name being
* validated doesn't match the expected value(s). Defaults to
* {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#WARNING}. Set this
* value to {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#INFORMATION}
* if you don't want to see display name validation issues at all in resource validation
* outcomes.
*
* @param theIssueSeverityForCodeDisplayMismatch The severity. Must not be {@literal null}.
* @since 7.0.0
*/
public void setIssueSeverityForCodeDisplayMismatch(@Nonnull IssueSeverity theIssueSeverityForCodeDisplayMismatch) {
Validate.notNull(
theIssueSeverityForCodeDisplayMismatch, "theIssueSeverityForCodeDisplayMismatch must not be null");
myIssueSeverityForCodeDisplayMismatch = theIssueSeverityForCodeDisplayMismatch;
}
@Override
public FhirContext getFhirContext() {
return myCtx;
@ -517,22 +555,26 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
.setCodeSystemName(codeSystemResourceName)
.setCodeSystemVersion(csVersion);
if (isNotBlank(theValueSetUrl)) {
codeValidationResult.setMessage(
"Code was validated against in-memory expansion of ValueSet: " + theValueSetUrl);
populateSourceDetailsForInMemoryExpansion(theValueSetUrl, codeValidationResult);
}
return codeValidationResult;
} else {
String message = "Concept Display \"" + theDisplayToValidate + "\" does not match expected \""
+ nextExpansionCode.getDisplay() + "\"";
String messageAppend = "";
if (isNotBlank(theValueSetUrl)) {
message += " for in-memory expansion of ValueSet: " + theValueSetUrl;
messageAppend = " for in-memory expansion of ValueSet: " + theValueSetUrl;
}
return new CodeValidationResult()
.setSeverity(IssueSeverity.ERROR)
.setDisplay(nextExpansionCode.getDisplay())
.setMessage(message)
.setCodeSystemName(codeSystemResourceName)
.setCodeSystemVersion(csVersion);
CodeValidationResult codeValidationResult = createResultForDisplayMismatch(
myCtx,
theCodeToValidate,
theDisplayToValidate,
nextExpansionCode.getDisplay(),
csVersion,
messageAppend,
getIssueSeverityForCodeDisplayMismatch());
if (isNotBlank(theValueSetUrl)) {
populateSourceDetailsForInMemoryExpansion(theValueSetUrl, codeValidationResult);
}
return codeValidationResult;
}
}
}
@ -1194,24 +1236,59 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
return theVersion;
}
public enum FailureType {
UNKNOWN_CODE_SYSTEM,
OTHER
private static void populateSourceDetailsForInMemoryExpansion(
String theValueSetUrl, CodeValidationResult codeValidationResult) {
codeValidationResult.setSourceDetails(
"Code was validated against in-memory expansion of ValueSet: " + theValueSetUrl);
}
public static class ExpansionCouldNotBeCompletedInternallyException extends Exception {
public static CodeValidationResult createResultForDisplayMismatch(
FhirContext theFhirContext,
String theCode,
String theDisplay,
String theExpectedDisplay,
String theCodeSystemVersion,
IssueSeverity theIssueSeverityForCodeDisplayMismatch) {
return createResultForDisplayMismatch(
theFhirContext,
theCode,
theDisplay,
theExpectedDisplay,
theCodeSystemVersion,
"",
theIssueSeverityForCodeDisplayMismatch);
}
private static final long serialVersionUID = -2226561628771483085L;
private final FailureType myFailureType;
private static CodeValidationResult createResultForDisplayMismatch(
FhirContext theFhirContext,
String theCode,
String theDisplay,
String theExpectedDisplay,
String theCodeSystemVersion,
String theMessageAppend,
IssueSeverity theIssueSeverityForCodeDisplayMismatch) {
public ExpansionCouldNotBeCompletedInternallyException(String theMessage, FailureType theFailureType) {
super(theMessage);
myFailureType = theFailureType;
}
public FailureType getFailureType() {
return myFailureType;
String message;
IssueSeverity issueSeverity = theIssueSeverityForCodeDisplayMismatch;
if (issueSeverity == IssueSeverity.INFORMATION) {
message = null;
issueSeverity = null;
} else {
message = theFhirContext
.getLocalizer()
.getMessage(
InMemoryTerminologyServerValidationSupport.class,
"displayMismatch",
theDisplay,
theExpectedDisplay)
+ theMessageAppend;
}
return new CodeValidationResult()
.setSeverity(issueSeverity)
.setMessage(message)
.setCode(theCode)
.setCodeSystemVersion(theCodeSystemVersion)
.setDisplay(theExpectedDisplay);
}
private static void flattenAndConvertCodesDstu2(
@ -1273,4 +1350,24 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
flattenAndConvertCodesR5(next.getContains(), theFhirVersionIndependentConcepts);
}
}
public enum FailureType {
UNKNOWN_CODE_SYSTEM,
OTHER
}
public static class ExpansionCouldNotBeCompletedInternallyException extends Exception {
private static final long serialVersionUID = -2226561628771483085L;
private final FailureType myFailureType;
public ExpansionCouldNotBeCompletedInternallyException(String theMessage, FailureType theFailureType) {
super(theMessage);
myFailureType = theFailureType;
}
public FailureType getFailureType() {
return myFailureType;
}
}
}

View File

@ -76,11 +76,12 @@ public class UnknownCodeSystemWarningValidationSupport extends BaseValidationSup
result.setSeverity(myNonExistentCodeSystemSeverity);
result.setMessage("CodeSystem is unknown and can't be validated: " + theCodeSystem);
// For information level, we just strip out the severity+message entirely
// so that nothing appears in the validation result
if (myNonExistentCodeSystemSeverity == IssueSeverity.INFORMATION) {
// for warnings, we don't set the code
// cause if we do, the severity is stripped out
// (see VersionSpecificWorkerContextWrapper.convertValidationResult)
result.setCode(theCode);
result.setSeverity(null);
result.setMessage(null);
}
return result;

View File

@ -37,7 +37,13 @@ abstract class BaseValidatorBridge implements IValidatorModule {
ResultSeverityEnum.fromCode(riMessage.getLevel().toCode()));
}
if (riMessage.getMessageId() != null) {
hapiMessage.setMessageId(riMessage.getMessageId());
// In BaseValidator, the messageId gets populated with the raw message because
// there is an assumption that it's a message key and not an actual message. But
// messsages coming from our internal terminology service don't work that
// way, so we strip them by checking if the ID is actually a sentence
if (!riMessage.getMessageId().contains(" ")) {
hapiMessage.setMessageId(riMessage.getMessageId());
}
}
if (riMessage.sliceText != null && riMessage.sliceText.length > 0) {
hapiMessage.setSliceMessages(Arrays.asList(riMessage.sliceText));

View File

@ -255,23 +255,30 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo
String code = theResult.getCode();
String display = theResult.getDisplay();
String issueSeverity = theResult.getSeverityCode();
String issueSeverityCode = theResult.getSeverityCode();
String message = theResult.getMessage();
if (isNotBlank(code)) {
retVal = new ValidationResult(
theSystem,
null,
new org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent()
.setCode(code)
.setDisplay(display),
null);
} else if (isNotBlank(issueSeverity)) {
retVal = new ValidationResult(
ValidationMessage.IssueSeverity.fromCode(issueSeverity),
message,
TerminologyServiceErrorClass.UNKNOWN,
null);
ValidationMessage.IssueSeverity issueSeverity = null;
if (issueSeverityCode != null) {
issueSeverity = ValidationMessage.IssueSeverity.fromCode(issueSeverityCode);
} else if (isNotBlank(message)) {
issueSeverity = ValidationMessage.IssueSeverity.INFORMATION;
}
CodeSystem.ConceptDefinitionComponent conceptDefinitionComponent = null;
if (code != null) {
conceptDefinitionComponent = new CodeSystem.ConceptDefinitionComponent()
.setCode(code)
.setDisplay(display);
}
retVal = new ValidationResult(
issueSeverity,
message,
theSystem,
theResult.getCodeSystemVersion(),
conceptDefinitionComponent,
display,
null);
}
if (retVal == null) {
@ -684,6 +691,32 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo
throw new UnsupportedOperationException(Msg.code(650) + "Unable to fetch resources of type: " + theClass);
}
@Override
public boolean isForPublication() {
return false;
}
@Override
public void setForPublication(boolean b) {
throw new UnsupportedOperationException(Msg.code(2351));
}
public static ConceptValidationOptions convertConceptValidationOptions(ValidationOptions theOptions) {
ConceptValidationOptions retVal = new ConceptValidationOptions();
if (theOptions.isGuessSystem()) {
retVal = retVal.setInferSystem(true);
}
return retVal;
}
@Nonnull
public static VersionSpecificWorkerContextWrapper newVersionSpecificWorkerContextWrapper(
IValidationSupport theValidationSupport) {
VersionCanonicalizer versionCanonicalizer = new VersionCanonicalizer(theValidationSupport.getFhirContext());
return new VersionSpecificWorkerContextWrapper(
new ValidationSupportContext(theValidationSupport), versionCanonicalizer);
}
private static class ResourceKey {
private final int myHashCode;
private final String myResourceName;
@ -729,30 +762,4 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo
return myHashCode;
}
}
public static ConceptValidationOptions convertConceptValidationOptions(ValidationOptions theOptions) {
ConceptValidationOptions retVal = new ConceptValidationOptions();
if (theOptions.isGuessSystem()) {
retVal = retVal.setInferSystem(true);
}
return retVal;
}
@Nonnull
public static VersionSpecificWorkerContextWrapper newVersionSpecificWorkerContextWrapper(
IValidationSupport theValidationSupport) {
VersionCanonicalizer versionCanonicalizer = new VersionCanonicalizer(theValidationSupport.getFhirContext());
return new VersionSpecificWorkerContextWrapper(
new ValidationSupportContext(theValidationSupport), versionCanonicalizer);
}
@Override
public boolean isForPublication() {
return false;
}
@Override
public void setForPublication(boolean b) {
throw new UnsupportedOperationException(Msg.code(2351));
}
}

View File

@ -22,6 +22,9 @@ import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -62,7 +65,7 @@ public class InMemoryTerminologyServerValidationSupportTest {
// ValidateCode
outcome = myChain.validateCode(valCtx, options, null, "txt", null, valueSetUrl);
assertTrue(outcome.isOk());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://hl7.org/fhir/ValueSet/mimetypes", outcome.getMessage());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://hl7.org/fhir/ValueSet/mimetypes", outcome.getSourceDetails());
assertEquals("txt", outcome.getCode());
// ValidateCodeInValueSet
@ -70,7 +73,7 @@ public class InMemoryTerminologyServerValidationSupportTest {
assertNotNull(valueSet);
outcome = myChain.validateCodeInValueSet(valCtx, options, null, "txt", null, valueSet);
assertTrue(outcome.isOk());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://hl7.org/fhir/ValueSet/mimetypes", outcome.getMessage());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://hl7.org/fhir/ValueSet/mimetypes", outcome.getSourceDetails());
assertEquals("txt", outcome.getCode());
}
@ -91,7 +94,7 @@ public class InMemoryTerminologyServerValidationSupportTest {
IValidationSupport.CodeValidationResult outcome;
outcome = myChain.validateCodeInValueSet(valCtx, options, "http://cs", "code1", null, vs);
assertEquals("Code was validated against in-memory expansion of ValueSet: http://vs", outcome.getMessage());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://vs", outcome.getSourceDetails());
assertTrue(outcome.isOk());
outcome = myChain.validateCodeInValueSet(valCtx, options, "http://cs", "code99", null, vs);
@ -127,7 +130,9 @@ public class InMemoryTerminologyServerValidationSupportTest {
IValidationSupport.CodeValidationResult outcome;
outcome = myChain.validateCodeInValueSet(valCtx, options, "http://cs", "code1", null, vs);
assertEquals("Code was validated against in-memory expansion of ValueSet: http://vs", outcome.getMessage());
assertNull(outcome.getMessage());
assertNull(outcome.getSeverityCode());
assertEquals("Code was validated against in-memory expansion of ValueSet: http://vs", outcome.getSourceDetails());
assertTrue(outcome.isOk());
outcome = myChain.validateCodeInValueSet(valCtx, options, "http://cs", "code99", null, vs);
@ -243,10 +248,13 @@ public class InMemoryTerminologyServerValidationSupportTest {
code = "28571000087109";
display = "BLAH";
outcome = mySvc.validateCode(valCtx, options, codeSystemUrl, code, display, valueSetUrl);
assertFalse(outcome.isOk());
assertEquals(null, outcome.getCode());
assertTrue(outcome.isOk());
assertEquals("28571000087109", outcome.getCode());
assertEquals("MODERNA COVID-19 mRNA-1273", outcome.getDisplay());
assertEquals("0.17", outcome.getCodeSystemVersion());
assertThat(outcome.getMessage(), containsString("Concept Display \"BLAH\" does not match expected \"MODERNA COVID-19 mRNA-1273\""));
assertEquals("warning", outcome.getSeverityCode());
assertThat(outcome.getSourceDetails(), startsWith("Code was validated against in-memory expansion"));
// Validate code - good code, good display
codeSystemUrl = "http://snomed.info/sct";

View File

@ -798,7 +798,7 @@ public class FhirInstanceValidatorDstu3Test {
Patient resource = loadResource("/dstu3/nl/nl-core-patient-01.json", Patient.class);
ValidationResult results = myVal.validateWithResult(resource);
List<SingleValidationMessage> outcome = logResultsAndReturnNonInformationalOnes(results);
assertThat(outcome.toString(), containsString("Could not confirm that the codes provided are in the value set 'LandGBACodelijst'"));
assertThat(outcome.toString(), containsString("The Coding provided (urn:oid:2.16.840.1.113883.2.4.4.16.34#6030) is not in the value set 'LandGBACodelijst'"));
}
private void loadNL() throws IOException {