diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java index 6ae1939f611..b08d10cadc5 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java @@ -553,11 +553,29 @@ public interface IValidationSupport { private String myCodeSystemVersion; private List 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; } diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index 17632574a82..f0375733e44 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -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. @@ -6,8 +9,8 @@ ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetExpandedUsingPreExpansion=ValueSet 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}" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5321-code-display-validation-now-configurable.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5321-code-display-validation-now-configurable.yaml new file mode 100644 index 00000000000..1e5b06ad12e --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5321-code-display-validation-now-configurable.yaml @@ -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." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/MdmJpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/MdmJpaConfig.java index 5d1178a1754..c9727845114 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/MdmJpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/MdmJpaConfig.java @@ -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; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java index 3cbd8bc5ddf..5fffa5d73c6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java @@ -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); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ValueSetOperationProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ValueSetOperationProvider.java index 816334e17ac..99189c92154 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ValueSetOperationProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ValueSetOperationProvider.java @@ -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 theSystem, @OperationParam(name = "systemVersion", min = 0, max = 1, typeName = "string") IPrimitiveType theSystemVersion, - @OperationParam(name = "display", min = 0, max = 1, typeName = "string") IPrimitiveType theDisplay, + @OperationParam(name = DISPLAY, min = 0, max = 1, typeName = "string") IPrimitiveType 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; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java index 3ace1fbb072..615c6fe57df 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java @@ -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; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java index 9a42f1d5873..8f88c3a88ec 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java @@ -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); diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmSurvivorshipConfig.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmSurvivorshipConfig.java index 9b9083cdbab..81f79c2e86e 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmSurvivorshipConfig.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmSurvivorshipConfig.java @@ -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; diff --git a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoValueSetDstu2Test.java b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoValueSetDstu2Test.java index 1700d1adc54..6551761c69d 100644 --- a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoValueSetDstu2Test.java +++ b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoValueSetDstu2Test.java @@ -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 diff --git a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValueSetTest.java b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValueSetTest.java index 38b22ee0291..73d3d7afeab 100644 --- a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValueSetTest.java +++ b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValueSetTest.java @@ -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()); } diff --git a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java index 5a1465e79de..09ce4621a13 100644 --- a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java +++ b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java @@ -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 diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java index b8ece1bac31..8bdf7f95a9b 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java @@ -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("
hello
"); + 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 */ diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java index a95aa057d15..7faf5984eea 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java @@ -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()); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CodeSystemTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CodeSystemTest.java index d5f1e93d872..24958f4153b 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CodeSystemTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CodeSystemTest.java @@ -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 diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetNoVerCSNoVerTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetNoVerCSNoVerTest.java index 76e917ae3f4..3ac4d4ed784 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetNoVerCSNoVerTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetNoVerCSNoVerTest.java @@ -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 diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java index a83494de830..ef4e4b36e2b 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java @@ -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; diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/term/ValueSetExpansionR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/term/ValueSetExpansionR4Test.java index fe4f2a22ae5..bc22114e25f 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/term/ValueSetExpansionR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/term/ValueSetExpansionR4Test.java @@ -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"; diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5ValueSetTest.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5ValueSetTest.java index ac5d9acc283..d27a8972181 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5ValueSetTest.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5ValueSetTest.java @@ -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()); } diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java index 37829a81f71..6d9b8a299c7 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java @@ -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 diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSurvivorshipSvcImpl.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSurvivorshipSvcImpl.java index 23c299736f3..99b56c5215f 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSurvivorshipSvcImpl.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSurvivorshipSvcImpl.java @@ -1,6 +1,6 @@ /*- * #%L - * HAPI FHIR JPA Server - Master Data Management + * HAPI FHIR - Master Data Management * %% * Copyright (C) 2014 - 2023 Smile CDR, Inc. * %% diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/mail/MailSvcIT.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/mail/MailSvcIT.java index c62c965f250..87f039401cb 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/mail/MailSvcIT.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/mail/MailSvcIT.java @@ -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()); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java index 2c282da6b70..23f7451be70 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java @@ -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. *

@@ -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. *

diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/models/TestResource.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/models/TestResource.java index 46b8604951a..a19f9d0ed50 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/models/TestResource.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/models/TestResource.java @@ -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; diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java index f44af8e3702..935099d573d 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java @@ -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; + } + } } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java index 26617914305..3d982b604b9 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java @@ -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; diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/BaseValidatorBridge.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/BaseValidatorBridge.java index 3f6e13c80ea..4653100aa55 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/BaseValidatorBridge.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/BaseValidatorBridge.java @@ -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)); diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java index c12fea3f5b4..370e1ea89b4 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java @@ -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)); - } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupportTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupportTest.java index 2953280fbf0..1a8f683a325 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupportTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupportTest.java @@ -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"; diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java index 394936c3dcb..86b6f028ad8 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java @@ -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 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 {