Correct validation of fragment codesystems (#1865)
* Update validation for fragment codesystem * Test fixes * Add changelog * Test fix
This commit is contained in:
parent
0175396ee0
commit
6899604ae4
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: add
|
||||
issue: 1865
|
||||
title: "The validator will now correctly issue a warning instead of an error if a code can't be found when validating a
|
||||
code in a CodeSystem where the content mode (CodeSystem.content) has a value of *fragment*."
|
|
@ -67,7 +67,7 @@ public class TermReadSvcR4 extends BaseTermReadSvcImpl implements ITermReadSvcR4
|
|||
|
||||
@Transactional(dontRollbackOn = {ExpansionTooCostlyException.class})
|
||||
@Override
|
||||
public IValidationSupport.ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, IBaseResource theValueSetToExpand) {
|
||||
public IValidationSupport.ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, IBaseResource theValueSetToExpand) {
|
||||
ValueSet expanded = super.expandValueSet(theExpansionOptions, (ValueSet) theValueSetToExpand);
|
||||
return new IValidationSupport.ValueSetExpansionOutcome(expanded);
|
||||
}
|
||||
|
@ -110,21 +110,21 @@ public class TermReadSvcR4 extends BaseTermReadSvcImpl implements ITermReadSvcR4
|
|||
}
|
||||
|
||||
if (!haveValidated) {
|
||||
TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager);
|
||||
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
|
||||
codeOpt = txTemplate.execute(t -> findCode(theCodeSystem, theCode).map(c->c.toVersionIndependentConcept()));
|
||||
TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager);
|
||||
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
|
||||
codeOpt = txTemplate.execute(t -> findCode(theCodeSystem, theCode).map(c -> c.toVersionIndependentConcept()));
|
||||
}
|
||||
|
||||
if (codeOpt != null && codeOpt.isPresent()) {
|
||||
VersionIndependentConcept code = codeOpt.get();
|
||||
IValidationSupport.CodeValidationResult retVal = new IValidationSupport.CodeValidationResult()
|
||||
.setCode(code.getCode()); // AAAAAAAAAAA format
|
||||
return retVal;
|
||||
}
|
||||
IValidationSupport.CodeValidationResult retVal = new IValidationSupport.CodeValidationResult()
|
||||
.setCode(code.getCode());
|
||||
return retVal;
|
||||
}
|
||||
|
||||
return new IValidationSupport.CodeValidationResult()
|
||||
.setSeverity(IssueSeverity.ERROR)
|
||||
.setMessage("Unknown code {" + theCodeSystem + "}" + theCode);
|
||||
return new IValidationSupport.CodeValidationResult()
|
||||
.setSeverity(IssueSeverity.ERROR)
|
||||
.setMessage("Unknown code {" + theCodeSystem + "}" + theCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -134,10 +134,71 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
|
|||
obs.getCode().getCodingFirstRep().setSystem("http://loinc.org").setCode("CODE3").setDisplay("Display 3");
|
||||
obs.getCategoryFirstRep().addCoding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("FOO");
|
||||
oo = validateAndReturnOutcome(obs);
|
||||
assertEquals(encode(oo), "Unknown code {http://terminology.hl7.org/CodeSystem/observation-category}FOO", oo.getIssueFirstRep().getDiagnostics());
|
||||
assertEquals(encode(oo), "Unknown code 'http://terminology.hl7.org/CodeSystem/observation-category#FOO'", oo.getIssueFirstRep().getDiagnostics());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Per: https://chat.fhir.org/#narrow/stream/179166-implementers/topic/Handling.20incomplete.20CodeSystems
|
||||
*
|
||||
* We should generate a warning if a code can't be found but the codesystem is a fragment
|
||||
*/
|
||||
@Test
|
||||
public void testValidateWithFragmentCodeSystem() throws IOException {
|
||||
myStructureDefinitionDao.create(loadResourceFromClasspath(StructureDefinition.class, "/r4/fragment/structuredefinition.json"));
|
||||
myCodeSystemDao.create(loadResourceFromClasspath(CodeSystem.class, "/r4/fragment/codesystem.json"));
|
||||
myValueSetDao.create(loadResourceFromClasspath(ValueSet.class, "/r4/fragment/valueset.json"));
|
||||
|
||||
createPatient(withId("A"), withActiveTrue());
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.setStatus(ObservationStatus.FINAL);
|
||||
obs.getSubject().setReference("Patient/A");
|
||||
obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED);
|
||||
obs.getText().getDiv().setValue("<div>hello</div>");
|
||||
obs.setValue(new StringType("hello"));
|
||||
obs.getPerformerFirstRep().setReference("Patient/A");
|
||||
obs.setEffective(new DateTimeType("2020-01-01"));
|
||||
|
||||
OperationOutcome outcome;
|
||||
|
||||
// Correct codesystem, but code not in codesystem
|
||||
obs.getCode().getCodingFirstRep().setSystem("http://example.com/codesystem");
|
||||
obs.getCode().getCodingFirstRep().setCode("foo-foo");
|
||||
obs.getCode().getCodingFirstRep().setDisplay("Some Code");
|
||||
outcome = (OperationOutcome) myObservationDao.validate(obs, null, null, null, ValidationModeEnum.CREATE, "http://example.com/structuredefinition", mySrd).getOperationOutcome();
|
||||
ourLog.info("Outcome: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
|
||||
assertEquals("Unknown code in fragment CodeSystem 'http://example.com/codesystem#foo-foo'", outcome.getIssueFirstRep().getDiagnostics());
|
||||
assertEquals(OperationOutcome.IssueSeverity.WARNING, outcome.getIssueFirstRep().getSeverity());
|
||||
|
||||
// Correct codesystem, Code in codesystem
|
||||
obs.getCode().getCodingFirstRep().setSystem("http://example.com/codesystem");
|
||||
obs.getCode().getCodingFirstRep().setCode("some-code");
|
||||
obs.getCode().getCodingFirstRep().setDisplay("Some Code");
|
||||
outcome = (OperationOutcome) myObservationDao.validate(obs, null, null, null, ValidationModeEnum.CREATE, "http://example.com/structuredefinition", mySrd).getOperationOutcome();
|
||||
ourLog.info("Outcome: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
|
||||
assertEquals("No issues detected during validation", outcome.getIssueFirstRep().getDiagnostics());
|
||||
assertEquals(OperationOutcome.IssueSeverity.INFORMATION, outcome.getIssueFirstRep().getSeverity());
|
||||
|
||||
// Code in wrong codesystem
|
||||
obs.getCode().getCodingFirstRep().setSystem("http://example.com/foo-foo");
|
||||
obs.getCode().getCodingFirstRep().setCode("some-code");
|
||||
obs.getCode().getCodingFirstRep().setDisplay("Some Code");
|
||||
try {
|
||||
outcome = (OperationOutcome) myObservationDao.validate(obs, null, null, null, ValidationModeEnum.CREATE, "http://example.com/structuredefinition", mySrd).getOperationOutcome();
|
||||
ourLog.info("Outcome: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
|
||||
assertEquals("", outcome.getIssueFirstRep().getDiagnostics());
|
||||
assertEquals(OperationOutcome.IssueSeverity.INFORMATION, outcome.getIssueFirstRep().getSeverity());
|
||||
fail();
|
||||
} catch (PreconditionFailedException e) {
|
||||
outcome = (OperationOutcome) e.getOperationOutcome();
|
||||
ourLog.info("Outcome: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
|
||||
assertEquals("None of the codes provided are in the value set http://example.com/valueset (http://example.com/valueset), and a code from this value set is required) (codes = http://example.com/foo-foo#some-code)", outcome.getIssueFirstRep().getDiagnostics());
|
||||
assertEquals(OperationOutcome.IssueSeverity.ERROR, outcome.getIssueFirstRep().getSeverity());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a loinc valueset that expands to more results than the expander is willing to do
|
||||
* in memory, and make sure we can still validate correctly, even if we're using
|
||||
|
@ -217,7 +278,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
|
|||
obs.getCode().getCodingFirstRep().setSystem("http://loinc.org").setCode("CODE3").setDisplay("Display 3");
|
||||
obs.getCategoryFirstRep().addCoding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("FOO");
|
||||
oo = validateAndReturnOutcome(obs);
|
||||
assertEquals(encode(oo), "Unknown code {http://terminology.hl7.org/CodeSystem/observation-category}FOO", oo.getIssueFirstRep().getDiagnostics());
|
||||
assertEquals(encode(oo), "Unknown code 'http://terminology.hl7.org/CodeSystem/observation-category#FOO'", oo.getIssueFirstRep().getDiagnostics());
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"resourceType": "CodeSystem",
|
||||
"id": "22472",
|
||||
"meta": {
|
||||
"versionId": "3",
|
||||
"lastUpdated": "2017-01-27T10:53:39.457-05:00"
|
||||
},
|
||||
"url": "http://example.com/codesystem",
|
||||
"name": "AddressUse",
|
||||
"status": "active",
|
||||
"publisher": "x1v1-mci",
|
||||
"date": "2016-04-07",
|
||||
"description": "AddressUse",
|
||||
"caseSensitive": true,
|
||||
"compositional": false,
|
||||
"versionNeeded": false,
|
||||
"content": "fragment",
|
||||
"concept": [
|
||||
{
|
||||
"code": "some-code",
|
||||
"display": "Some Code",
|
||||
"definition": "Blah blah"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"resourceType": "StructureDefinition",
|
||||
"id": "vitalsigns",
|
||||
"url": "http://example.com/structuredefinition",
|
||||
"version": "4.0.0",
|
||||
"name": "observation-vitalsigns",
|
||||
"title": "Vital Signs Profile",
|
||||
"status": "draft",
|
||||
"experimental": false,
|
||||
"date": "2016-03-25",
|
||||
"publisher": "Health Level Seven International (Orders and Observations Workgroup)",
|
||||
"description": "FHIR Vital Signs Profile",
|
||||
"fhirVersion": "4.0.0",
|
||||
"kind": "resource",
|
||||
"abstract": false,
|
||||
"type": "Observation",
|
||||
"baseDefinition": "http://hl7.org/fhir/StructureDefinition/Observation",
|
||||
"derivation": "constraint",
|
||||
"differential": {
|
||||
"element": [
|
||||
{
|
||||
"id": "Observation",
|
||||
"path": "Observation",
|
||||
"short": "FHIR Vital Signs Profile",
|
||||
"definition": "The FHIR Vitals Signs profile sets minimum expectations for the Observation Resource to record, search and fetch the vital signs associated with a patient.",
|
||||
"alias": [
|
||||
"Vital Signs",
|
||||
"Measurement",
|
||||
"Results",
|
||||
"Tests"
|
||||
],
|
||||
"min": 0,
|
||||
"max": "*"
|
||||
},
|
||||
{
|
||||
"id": "Observation.code",
|
||||
"path": "Observation.code",
|
||||
"short": "Coded Responses from C-CDA Vital Sign Results",
|
||||
"definition": "Coded Responses from C-CDA Vital Sign Results.",
|
||||
"requirements": "5. SHALL contain exactly one [1..1] code, where the @code SHOULD be selected from ValueSet HITSP Vital Sign Result Type 2.16.840.1.113883.3.88.12.80.62 DYNAMIC (CONF:7301).",
|
||||
"min": 1,
|
||||
"max": "1",
|
||||
"type": [
|
||||
{
|
||||
"code": "CodeableConcept"
|
||||
}
|
||||
],
|
||||
"mustSupport": true,
|
||||
"binding": {
|
||||
"strength": "required",
|
||||
"description": "This identifies the vital sign result type.",
|
||||
"valueSet": "http://example.com/valueset"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"resourceType" : "ValueSet",
|
||||
"id" : "message-category",
|
||||
"url" : "http://example.com/valueset",
|
||||
"version" : "0.0.1",
|
||||
"name" : "MessageCategory",
|
||||
"status" : "active",
|
||||
"experimental" : true,
|
||||
"date" : "2019-02-08T00:00:00+00:00",
|
||||
"publisher" : "ehealth.sundhed.dk",
|
||||
"compose" : {
|
||||
"include" : [
|
||||
{
|
||||
"system" : "http://example.com/codesystem"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -222,6 +222,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
|
|||
|
||||
String codeSystemName = null;
|
||||
String codeSystemVersion = null;
|
||||
String codeSystemContentMode = null;
|
||||
if (system != null) {
|
||||
switch (system.getStructureFhirVersionEnum()) {
|
||||
case DSTU2_HL7ORG: {
|
||||
|
@ -233,6 +234,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
|
|||
caseSensitive = systemDstu3.getCaseSensitive();
|
||||
codeSystemName = systemDstu3.getName();
|
||||
codeSystemVersion = systemDstu3.getVersion();
|
||||
codeSystemContentMode = systemDstu3.getContentElement().getValueAsString();
|
||||
break;
|
||||
}
|
||||
case R4: {
|
||||
|
@ -240,6 +242,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
|
|||
caseSensitive = systemR4.getCaseSensitive();
|
||||
codeSystemName = systemR4.getName();
|
||||
codeSystemVersion = systemR4.getVersion();
|
||||
codeSystemContentMode = systemR4.getContentElement().getValueAsString();
|
||||
break;
|
||||
}
|
||||
case R5: {
|
||||
|
@ -247,6 +250,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
|
|||
caseSensitive = systemR5.getCaseSensitive();
|
||||
codeSystemName = systemR5.getName();
|
||||
codeSystemVersion = systemR5.getVersion();
|
||||
codeSystemContentMode = systemR5.getContentElement().getValueAsString();
|
||||
break;
|
||||
}
|
||||
case DSTU2:
|
||||
|
@ -275,9 +279,16 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
|
|||
}
|
||||
}
|
||||
|
||||
ValidationMessage.IssueSeverity severity = ValidationMessage.IssueSeverity.ERROR;
|
||||
ValidationMessage.IssueSeverity severity;
|
||||
String message;
|
||||
if ("fragment".equals(codeSystemContentMode)) {
|
||||
severity = ValidationMessage.IssueSeverity.WARNING;
|
||||
message = "Unknown code in fragment CodeSystem '" + (isNotBlank(theCodeSystem) ? theCodeSystem + "#" : "") + theCode + "'";
|
||||
} else {
|
||||
severity = ValidationMessage.IssueSeverity.ERROR;
|
||||
message = "Unknown code '" + (isNotBlank(theCodeSystem) ? theCodeSystem + "#" : "") + theCode + "'";
|
||||
}
|
||||
|
||||
String message = "Unknown code '" + (isNotBlank(theCodeSystem) ? theCodeSystem + "#" : "") + theCode + "'";
|
||||
return new CodeValidationResult()
|
||||
.setSeverityCode(severity.toCode())
|
||||
.setMessage(message);
|
||||
|
|
|
@ -236,9 +236,11 @@ public class ValidationSupportChain implements IValidationSupport {
|
|||
@Override
|
||||
public CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
|
||||
for (IValidationSupport next : myChain) {
|
||||
CodeValidationResult retVal = next.validateCodeInValueSet(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSet);
|
||||
if (retVal != null) {
|
||||
return retVal;
|
||||
if (theOptions.isInferSystem() || (theCodeSystem != null && next.isCodeSystemSupported(theValidationSupportContext, theCodeSystem))) {
|
||||
CodeValidationResult retVal = next.validateCodeInValueSet(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSet);
|
||||
if (retVal != null) {
|
||||
return retVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
|
|
@ -907,6 +907,14 @@ public class QuestionnaireResponseValidatorDstu3Test {
|
|||
|
||||
@Test
|
||||
public void testValidateQuestionnaireResponseWithValueSetChoiceAnswer() {
|
||||
/*
|
||||
* Create CodeSystem
|
||||
*/
|
||||
CodeSystem codeSystem = new CodeSystem();
|
||||
codeSystem.setContent(CodeSystemContentMode.COMPLETE);
|
||||
codeSystem.setUrl(SYSTEMURI_ICC_SCHOOLTYPE);
|
||||
codeSystem.addConcept().setCode(CODE_ICC_SCHOOLTYPE_PT);
|
||||
|
||||
/*
|
||||
* Create valueset
|
||||
*/
|
||||
|
@ -959,6 +967,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
|
|||
when(myValSupport.fetchResource(eq(ValueSet.class), eq(ID_VS_SCHOOLTYPE.getValue()))).thenReturn(iccSchoolTypeVs);
|
||||
when(myValSupport.validateCodeInValueSet(any(), any(), eq(SYSTEMURI_ICC_SCHOOLTYPE), eq(CODE_ICC_SCHOOLTYPE_PT), any(), nullable(ValueSet.class)))
|
||||
.thenReturn(new IValidationSupport.CodeValidationResult().setCode(CODE_ICC_SCHOOLTYPE_PT));
|
||||
when(myValSupport.fetchCodeSystem(eq(SYSTEMURI_ICC_SCHOOLTYPE))).thenReturn(codeSystem);
|
||||
|
||||
ValidationResult errors = myVal.validateWithResult(questionnaireResponse);
|
||||
|
||||
|
|
|
@ -733,6 +733,14 @@ public class QuestionnaireResponseValidatorR4Test {
|
|||
|
||||
@Test
|
||||
public void testValidateQuestionnaireResponseWithValueSetChoiceAnswer() {
|
||||
/*
|
||||
* Create CodeSystem
|
||||
*/
|
||||
CodeSystem codeSystem = new CodeSystem();
|
||||
codeSystem.setContent(CodeSystem.CodeSystemContentMode.COMPLETE);
|
||||
codeSystem.setUrl(SYSTEMURI_ICC_SCHOOLTYPE);
|
||||
codeSystem.addConcept().setCode(CODE_ICC_SCHOOLTYPE_PT);
|
||||
|
||||
/*
|
||||
* Create valueset
|
||||
*/
|
||||
|
@ -785,6 +793,7 @@ public class QuestionnaireResponseValidatorR4Test {
|
|||
when(myValSupport.fetchResource(eq(Questionnaire.class), eq(qa.getQuestionnaire()))).thenReturn(questionnaire);
|
||||
when(myValSupport.fetchResource(eq(ValueSet.class), eq(ID_VS_SCHOOLTYPE))).thenReturn(iccSchoolTypeVs);
|
||||
when(myValSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any(ValueSet.class))).thenReturn(new IValidationSupport.CodeValidationResult().setCode(CODE_ICC_SCHOOLTYPE_PT));
|
||||
when(myValSupport.fetchCodeSystem(eq(SYSTEMURI_ICC_SCHOOLTYPE))).thenReturn(codeSystem);
|
||||
ValidationResult errors = myVal.validateWithResult(qa);
|
||||
|
||||
ourLog.info(errors.toString());
|
||||
|
|
|
@ -625,6 +625,14 @@ public class QuestionnaireResponseValidatorR5Test {
|
|||
|
||||
@Test
|
||||
public void testValidateQuestionnaireResponseWithValueSetChoiceAnswer() {
|
||||
/*
|
||||
* Create CodeSystem
|
||||
*/
|
||||
CodeSystem codeSystem = new CodeSystem();
|
||||
codeSystem.setContent(CodeSystem.CodeSystemContentMode.COMPLETE);
|
||||
codeSystem.setUrl(SYSTEMURI_ICC_SCHOOLTYPE);
|
||||
codeSystem.addConcept().setCode(CODE_ICC_SCHOOLTYPE_PT);
|
||||
|
||||
/*
|
||||
* Create valueset
|
||||
*/
|
||||
|
@ -677,6 +685,7 @@ public class QuestionnaireResponseValidatorR5Test {
|
|||
when(myValSupport.fetchResource(eq(Questionnaire.class), eq(qa.getQuestionnaire()))).thenReturn(questionnaire);
|
||||
when(myValSupport.fetchResource(eq(ValueSet.class), eq(ID_VS_SCHOOLTYPE))).thenReturn(iccSchoolTypeVs);
|
||||
when(myValSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any(ValueSet.class))).thenReturn(new IValidationSupport.CodeValidationResult().setCode(CODE_ICC_SCHOOLTYPE_PT));
|
||||
when(myValSupport.fetchCodeSystem(eq(SYSTEMURI_ICC_SCHOOLTYPE))).thenReturn(codeSystem);
|
||||
ValidationResult errors = myVal.validateWithResult(qa);
|
||||
|
||||
ourLog.info(errors.toString());
|
||||
|
|
Loading…
Reference in New Issue