Handle unknown code systems properly

This commit is contained in:
Grahame Grieve 2023-05-22 17:16:08 +10:00
parent 7339d0ae1d
commit 073e89d98d
2 changed files with 113 additions and 47 deletions

View File

@ -11,6 +11,13 @@ import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
public class ValidationProcessInfo {
private TerminologyServiceErrorClass err;
private List<OperationOutcomeIssueComponent> issues = new ArrayList<>();
public ValidationProcessInfo() {
}
public ValidationProcessInfo(List<OperationOutcomeIssueComponent> issues) {
this.issues = issues;
}
public TerminologyServiceErrorClass getErr() {
return err;
}

View File

@ -100,6 +100,8 @@ public class ValueSetValidator {
private List<CodeSystem> localSystems = new ArrayList<>();
Parameters expansionProfile;
private TerminologyCapabilities txCaps;
private Set<String> unknownSystems;
private boolean throwToServer;
public ValueSetValidator(ValidationOptions options, ValueSet source, IWorkerContext context, Parameters expansionProfile, TerminologyCapabilities txCaps) {
this.valueset = source;
@ -120,6 +122,22 @@ public class ValueSetValidator {
analyseValueSet();
}
public Set<String> getUnknownSystems() {
return unknownSystems;
}
public void setUnknownSystems(Set<String> unknownSystems) {
this.unknownSystems = unknownSystems;
}
public boolean isThrowToServer() {
return throwToServer;
}
public void setThrowToServer(boolean throwToServer) {
this.throwToServer = throwToServer;
}
private void analyseValueSet() {
if (localContext != null) {
if (valueset != null) {
@ -174,10 +192,16 @@ public class ValueSetValidator {
if (context.isNoTerminologyServer()) {
if (c.hasVersion()) {
String msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM_VERSION, c.getSystem(), c.getVersion() , resolveCodeSystemVersions(c.getSystem()).toString());
res = new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".coding["+i+"].system", msg));
if (valueSetDependsOn(c.getSystem(), c.getVersion())) {
unknownSystems.add(c.getSystem()+"|"+c.getVersion());
}
res = new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".coding["+i+"].system", msg)).setUnknownSystems(unknownSystems);
} else {
String msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM, c.getSystem(), c.getVersion());
res = new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".coding["+i+"].system", msg));
if (valueSetDependsOn(c.getSystem(), null)) {
unknownSystems.add(c.getSystem());
}
res = new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".coding["+i+"].system", msg)).setUnknownSystems(unknownSystems);
}
} else {
res = context.validateCode(options.withNoClient(), c, null);
@ -191,8 +215,9 @@ public class ValueSetValidator {
}
}
Coding foundCoding = null;
String msg = null;
Boolean result = false;
if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) {
Boolean result = false;
CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(", ");
for (Coding c : code.getCoding()) {
@ -212,11 +237,11 @@ public class ValueSetValidator {
}
}
if (result == null) {
String msg = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl(), b.toString());
info.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.INVALID, path, msg));
msg = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), b.toString());
info.getIssues().addAll(makeIssue(IssueSeverity.WARNING, unknownSystems.isEmpty() ? IssueType.INVALID : IssueType.NOTFOUND, path, msg));
} else if (!result) {
String msg = context.formatMessagePlural(code.getCoding().size(), I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl(), b.toString());
info.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, msg));
msg = context.formatMessagePlural(code.getCoding().size(), I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), b.toString());
info.getIssues().addAll(makeIssue(IssueSeverity.ERROR, unknownSystems.isEmpty() ? IssueType.INVALID : IssueType.NOTFOUND, path, msg));
}
}
if (info.hasErrors()) {
@ -229,8 +254,11 @@ public class ValueSetValidator {
res.setVersion(foundCoding.hasVersion() ? foundCoding.getVersion() : ((CodeSystem) foundCoding.getUserData("cs")).getVersion());
res.setDisplay(cd.getDisplay());
}
res.setUnknownSystems(unknownSystems);
res.addCodeableConcept(vcc);
return res;
} else if (result == null) {
return new ValidationResult(IssueSeverity.WARNING, info.summary(), info.getIssues());
} else if (foundCoding == null) {
return new ValidationResult(IssueSeverity.ERROR, "Internal Error that should not happen", makeIssue(IssueSeverity.FATAL, IssueType.EXCEPTION, path, "Internal Error that should not happen"));
} else if (info.getIssues().size() > 0) {
@ -245,6 +273,15 @@ public class ValueSetValidator {
}
}
private boolean valueSetDependsOn(String system, String version) {
for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
if (system.equals(inc.getSystem()) && (version == null || inc.getVersion() == null || version.equals(inc.getVersion()))) {
return true;
}
}
return false;
}
private String getVersion(Coding c) {
if (c.hasVersion()) {
return c.getVersion();
@ -329,6 +366,7 @@ public class ValueSetValidator {
ValidationResult res = null;
boolean inExpansion = false;
boolean inInclude = false;
List<OperationOutcomeIssueComponent> issues = new ArrayList<>();
VersionInfo vi = new VersionInfo(this);
String system = code.hasSystem() ? code.getSystem() : getValueSetSystemOrNull();
@ -363,8 +401,10 @@ public class ValueSetValidator {
if (cs == null) {
if (wv == null) {
warningMessage = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM, system);
unknownSystems.add(system);
} else {
warningMessage = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM_VERSION, system, wv, resolveCodeSystemVersions(system).toString());
unknownSystems.add(system+"|"+wv);
}
if (!inExpansion) {
if (valueset != null && valueset.hasExpansion()) {
@ -372,18 +412,18 @@ public class ValueSetValidator {
valueset.getUrl(),
code.getSystem(),
code.getCode().toString());
List<OperationOutcomeIssueComponent> issues = new ArrayList<>();
issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path, msg));
throw new VSCheckerException(msg, issues);
} else {
List<OperationOutcomeIssueComponent> issues = new ArrayList<>();
issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".system", warningMessage));
res = new ValidationResult(IssueSeverity.WARNING, warningMessage, issues);
if (valueset == null) {
throw new VSCheckerException(warningMessage, issues);
} else {
String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl(), code.toString());
issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, msg));
throw new VSCheckerException(warningMessage+"; "+msg, issues);
// String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl(), code.toString());
// issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, msg));
// we don't do this yet
// throw new VSCheckerException(warningMessage, issues);
}
}
}
@ -407,7 +447,7 @@ public class ValueSetValidator {
// we'll take it on faith
String disp = getPreferredDisplay(cc);
res = new ValidationResult(system, cs.getVersion(), new ConceptDefinitionComponent().setCode(cc.getCode()).setDisplay(disp), disp);
res.setMessage("Resolved system "+system+", but the definition is not complete, so assuming value set include is correct");
res.addToMessage("Resolved system "+system+", but the definition is not complete, so assuming value set include is correct");
return res;
}
}
@ -419,12 +459,14 @@ public class ValueSetValidator {
// we just take the value set as face value then
res = new ValidationResult(system, wv, new ConceptDefinitionComponent().setCode(code.getCode()).setDisplay(code.getDisplay()), code.getDisplay());
if (!preferServerSide(system)) {
res.setMessage("Code System unknown, so assuming value set expansion is correct ("+warningMessage+")");
res.addToMessage("Code System unknown, so assuming value set expansion is correct ("+warningMessage+")");
}
} else {
// well, we didn't find a code system - try the expansion?
// disabled waiting for discussion
throw new FHIRException("No try the server");
if (throwToServer) {
throw new FHIRException("No; try the server");
}
}
} else {
inExpansion = checkExpansion(code, vi);
@ -432,7 +474,7 @@ public class ValueSetValidator {
}
String wv = vi.getVersion(system, code.getVersion());
ValidationProcessInfo info = new ValidationProcessInfo();
ValidationProcessInfo info = new ValidationProcessInfo(issues);
// then, if we have a value set, we check it's in the value set
if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) {
@ -446,20 +488,24 @@ public class ValueSetValidator {
res.setErrorClass(info.getErr());
}
if (ok == null) {
res.setMessage("Unable to check whether code is in value set "+valueset.getUrl()+": "+info.summary()).setSeverity(IssueSeverity.WARNING);
res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, res.getMessage()));
String m = "Unable to check whether the code is in the value set "+valueset.getVersionedUrl();
res.addToMessage(m);
res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.NOTFOUND, path, m));
res.setUnknownSystems(unknownSystems);
res.setSeverity(IssueSeverity.ERROR); // back patching for display logic issue
} else if (!inExpansion && !inInclude) {
if (!info.getIssues().isEmpty()) {
res.setMessage("Not in value set "+valueset.getUrl()+": "+info.summary()).setSeverity(IssueSeverity.ERROR);
res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, res.getMessage()));
} else {
String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl(), code.toString());
res.setMessage(msg).setSeverity(IssueSeverity.ERROR);
// if (!info.getIssues().isEmpty()) {
// res.setMessage("Not in value set "+valueset.getUrl()+": "+info.summary()).setSeverity(IssueSeverity.ERROR);
// res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, res.getMessage()));
// } else
// {
String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), code.toString());
res.addToMessage(msg).setSeverity(IssueSeverity.ERROR);
res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, msg));
res.setDefinition(null);
res.setSystem(null);
res.setDisplay(null);
}
// }
} else if (warningMessage!=null) {
String msg = context.formatMessage(I18nConstants.CODE_FOUND_IN_EXPANSION_HOWEVER_, warningMessage);
res = new ValidationResult(IssueSeverity.WARNING, msg, makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, msg));
@ -472,7 +518,7 @@ public class ValueSetValidator {
}
}
} else if ((res != null && !res.isOk())) {
String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl(), code.toString());
String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), code.toString());
res.setMessage(res.getMessage()+"; "+msg);
res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, msg));
}
@ -483,7 +529,7 @@ public class ValueSetValidator {
return res;
}
private static final HashSet<String> SERVER_SIDE_LIST = new HashSet<>(Arrays.asList("http://fdasis.nlm.nih.gov", "http://hl7.org/fhir/sid/ndc", "http://loinc.org", "http://snomed.info/sct", "http://unitsofmeasure.org",
private static final Set<String> SERVER_SIDE_LIST = new HashSet<>(Arrays.asList("http://fdasis.nlm.nih.gov", "http://hl7.org/fhir/sid/ndc", "http://loinc.org", "http://snomed.info/sct", "http://unitsofmeasure.org",
"http://unstats.un.org/unsd/methods/m49/m49.htm", "http://varnomen.hgvs.org", "http://www.nlm.nih.gov/research/umls/rxnorm", "https://www.usps.com/",
"urn:ietf:bcp:13","urn:ietf:bcp:47","urn:ietf:rfc:3986", "urn:iso:std:iso:3166","urn:iso:std:iso:4217", "urn:oid:1.2.36.1.2001.1005.17"));
@ -917,6 +963,7 @@ public class ValueSetValidator {
public Boolean codeInValueSet(String system, String version, String code, ValidationProcessInfo info) throws FHIRException {
return codeInValueSet("code", system, version, code, info);
}
public Boolean codeInValueSet(String path, String system, String version, String code, ValidationProcessInfo info) throws FHIRException {
if (valueset == null) {
return false;
@ -991,30 +1038,42 @@ public class ValueSetValidator {
// ok, we need the code system
CodeSystem cs = resolveCodeSystem(system, version);
if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) {
// make up a transient value set with
ValueSet vs = new ValueSet();
vs.setStatus(PublicationStatus.ACTIVE);
vs.setUrl(valueset.getUrl()+"--"+vsiIndex);
vs.setVersion(valueset.getVersion());
vs.getCompose().addInclude(vsi);
ValidationResult res = context.validateCode(options.withNoClient(), new Coding(system, code, null), vs);
if (res.getErrorClass() == TerminologyServiceErrorClass.UNKNOWN || res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED || res.getErrorClass() == TerminologyServiceErrorClass.VALUESET_UNSUPPORTED) {
if (info != null && res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) {
// server didn't know the code system either - we'll take it face value
info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.UNKNOWN, path, context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_NOTKNOWN, system)));
for (ConceptReferenceComponent cc : vsi.getConcept()) {
if (cc.getCode().equals(code)) {
return true;
if (throwToServer) {
// make up a transient value set with
ValueSet vs = new ValueSet();
vs.setStatus(PublicationStatus.ACTIVE);
vs.setUrl(valueset.getUrl()+"--"+vsiIndex);
vs.setVersion(valueset.getVersion());
vs.getCompose().addInclude(vsi);
ValidationResult res = context.validateCode(options.withNoClient(), new Coding(system, code, null), vs);
if (res.getErrorClass() == TerminologyServiceErrorClass.UNKNOWN || res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED || res.getErrorClass() == TerminologyServiceErrorClass.VALUESET_UNSUPPORTED) {
if (info != null && res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) {
// server didn't know the code system either - we'll take it face value
info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.UNKNOWN, path, context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_NOTKNOWN, system)));
for (ConceptReferenceComponent cc : vsi.getConcept()) {
if (cc.getCode().equals(code)) {
return true;
}
}
info.setErr(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
return null;
}
info.setErr(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
return false;
}
return false;
if (res.getErrorClass() == TerminologyServiceErrorClass.NOSERVICE) {
throw new NoTerminologyServiceException();
}
return res.isOk();
} else {
if (unknownSystems != null) {
if (version == null) {
unknownSystems.add(system);
} else {
unknownSystems.add(system+"|"+version);
}
}
return null;
}
if (res.getErrorClass() == TerminologyServiceErrorClass.NOSERVICE) {
throw new NoTerminologyServiceException();
}
return res.isOk();
} else {
if (vsi.hasFilter()) {
ok = true;