Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -553,11 +553,29 @@ class CodeValidationResult {
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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@

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.
ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetExpandedUsingPreExpansion=ValueSet was expanded using an expansion that was pre-calculated at {0}
ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetExpandedUsingInMemoryExpansion=ValueSet with URL "{0}" was expanded using an in-memory expansion
ca.uhn.fhir.jpa.term.TermReadSvcImpl.validationPerformedAgainstPreExpansion=Code validation occurred using a ValueSet expansion that was pre-calculated at {0}
ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetNotFoundInTerminologyDatabase=ValueSet can not be found in terminology database: {0}
ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetPreExpansionInvalidated=ValueSet with URL "{0}" precaluclated expansion with {1} concept(s) has been invalidated
ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetCantInvalidateNotYetPrecalculated=ValueSet with URL "{0}" already has status: {1}
ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetPreExpansionInvalidated=ValueSet with URL "{0}" precaluclated expansion with {1} concept(s) has been invalidated
ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetCantInvalidateNotYetPrecalculated=ValueSet with URL "{0}" already has status: {1}
ca.uhn.fhir.jpa.term.TermReadSvcImpl.unknownCodeInSystem=Unknown code "{0}#{1}"


Expand Down
Original file line number Diff line number Diff line change
@@ -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."
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@
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;
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;
Expand All @@ -45,6 +47,15 @@ public DefaultProfileValidationSupport defaultProfileValidationSupport(FhirConte
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
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;
Expand Down Expand Up @@ -145,9 +149,10 @@ protected IFhirResourceDaoValueSet<IBaseResource> getDao() {
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,
Expand All @@ -159,7 +164,7 @@ public IBaseParameters validateCode(
@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,
Expand Down Expand Up @@ -251,7 +256,7 @@ private Supplier<CodeValidationResult> supplyUnableToValidateResult(
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);
Expand All @@ -260,7 +265,7 @@ public IBaseParameters invalidateValueSetExpansion(
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 {
Expand Down Expand Up @@ -325,12 +330,16 @@ public static ValueSetExpansionOptions createValueSetExpansionOptions(
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -1025,11 +1028,8 @@ private void expandValueSetHandleIncludeOrExclude(
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()
Expand Down Expand Up @@ -2055,7 +2055,7 @@ protected IValidationSupport.CodeValidationResult validateCodeIsInPreExpandedVal
.findByResourcePid(valueSetResourcePid.getId())
.orElseThrow(IllegalStateException::new);
String timingDescription = toHumanReadableExpansionTimestamp(valueSetEntity);
String msg = myContext
String preExpansionMessage = myContext
.getLocalizer()
.getMessage(TermReadSvcImpl.class, "validationPerformedAgainstPreExpansion", timingDescription);

Expand All @@ -2068,22 +2068,26 @@ protected IValidationSupport.CodeValidationResult validateCodeIsInPreExpandedVal
.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()) {
return new IValidationSupport.CodeValidationResult()
.setCode(concepts.get(0).getCode())
.setDisplay(concepts.get(0).getDisplay())
.setCodeSystemVersion(concepts.get(0).getSystemVersion())
.setMessage(msg);
.setMessage(preExpansionMessage);
}

// Ok, we failed
Expand All @@ -2096,7 +2100,7 @@ protected IValidationSupport.CodeValidationResult validateCodeIsInPreExpandedVal
String unknownCodeMessage = myContext
.getLocalizer()
.getMessage(TermReadSvcImpl.class, "unknownCodeInSystem", theSystem, theCode);
append = " - " + unknownCodeMessage + ". " + msg;
append = " - " + unknownCodeMessage + ". " + preExpansionMessage;
}

return createFailureCodeValidationResult(theSystem, theCode, null, append);
Expand Down Expand Up @@ -2710,11 +2714,13 @@ public IValidationSupport.CodeValidationResult validateCode(
|| 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());
}
}

Expand Down Expand Up @@ -2752,14 +2758,13 @@ IValidationSupport.CodeValidationResult validateCodeInValueSet(

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);
Expand Down Expand Up @@ -3182,13 +3187,6 @@ static boolean isValueSetDisplayLanguageMatch(ValueSetExpansionOptions theExpans
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ public class JpaValidationSupportChain extends ValidationSupportChain {
@Autowired
private UnknownCodeSystemWarningValidationSupport myUnknownCodeSystemWarningValidationSupport;

@Autowired
private InMemoryTerminologyServerValidationSupport myInMemoryTerminologyServerValidationSupport;

/**
* Constructor
*/
Expand All @@ -82,7 +85,7 @@ public void postConstruct() {
addValidationSupport(myJpaValidationSupport);
addValidationSupport(myTerminologyService);
addValidationSupport(new SnapshotGeneratingValidationSupport(myFhirContext));
addValidationSupport(new InMemoryTerminologyServerValidationSupport(myFhirContext));
addValidationSupport(myInMemoryTerminologyServerValidationSupport);
addValidationSupport(myNpmJpaValidationSupport);
addValidationSupport(new CommonCodeSystemsTerminologyService(myFhirContext));
addValidationSupport(myConceptMappingSvc);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,10 @@ public void testValidateCodeOperationByIdentifierAndCodeAndSystemAndBadDisplay()
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ public void testValidateCodeOperationByIdentifierAndCodeAndSystemAndBadDisplay()
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());
}

Expand Down
Loading