Correct validation of fragment codesystems (#1865)

* Update validation for fragment codesystem

* Test fixes

* Add changelog

* Test fix
This commit is contained in:
James Agnew 2020-05-25 20:44:09 -04:00 committed by GitHub
parent 0175396ee0
commit 6899604ae4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 224 additions and 18 deletions

View File

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

View File

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

View File

@ -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());
}

View File

@ -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"
}
]
}

View File

@ -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"
}
}
]
}
}

View File

@ -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"
}
]
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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());

View File

@ -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());