From 32f2f6acc7cbaef3a6403279e190ae52c3dcc323 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Wed, 2 Aug 2023 18:12:02 +1000 Subject: [PATCH] warnings for status on terminology resources --- .../fhir/r5/context/BaseWorkerContext.java | 27 ++++- .../hl7/fhir/r5/context/IWorkerContext.java | 10 +- .../org/hl7/fhir/r5/model/Parameters.java | 8 ++ .../java/org/hl7/fhir/r5/model/ValueSet.java | 20 +++- .../expansion/ValueSetExpander.java | 68 ++++++------ .../utilities/ValueSetProcessBase.java | 101 ++++++++++++++++-- .../validation/ValueSetValidator.java | 49 +++------ .../hl7/fhir/utilities/StandardsStatus.java | 16 ++- .../fhir/utilities/i18n/I18nConstants.java | 3 + .../src/main/resources/Messages.properties | 5 +- .../validation/special/TxTesterSorters.java | 29 ++++- 11 files changed, 248 insertions(+), 88 deletions(-) diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java index 13d21dd32..2962e8908 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java @@ -1490,6 +1490,8 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte String system = null; String code = null; String version = null; + List issues = new ArrayList<>(); + TerminologyServiceErrorClass err = TerminologyServiceErrorClass.UNKNOWN; for (ParametersParameterComponent p : pOut.getParameter()) { if (p.hasValue()) { @@ -1506,7 +1508,19 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte } else if (p.getName().equals("code")) { code = ((PrimitiveType) p.getValue()).asStringValue(); } else if (p.getName().equals("x-caused-by-unknown-system")) { - err = TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED; + err = TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED; + } else if (p.getName().equals("warning-withdrawn")) { + OperationOutcomeIssueComponent iss = new OperationOutcomeIssueComponent(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.INFORMATION, org.hl7.fhir.r5.model.OperationOutcome.IssueType.EXPIRED); + iss.getDetails().setText(formatMessage(I18nConstants.MSG_WITHDRAWN, ((PrimitiveType) p.getValue()).asStringValue())); + issues.add(iss); + } else if (p.getName().equals("warning-deprecated")) { + OperationOutcomeIssueComponent iss = new OperationOutcomeIssueComponent(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.INFORMATION, org.hl7.fhir.r5.model.OperationOutcome.IssueType.EXPIRED); + iss.getDetails().setText(formatMessage(I18nConstants.MSG_DEPRECATED, ((PrimitiveType) p.getValue()).asStringValue())); + issues.add(iss); + } else if (p.getName().equals("warning-retired")) { + OperationOutcomeIssueComponent iss = new OperationOutcomeIssueComponent(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.INFORMATION, org.hl7.fhir.r5.model.OperationOutcome.IssueType.EXPIRED); + iss.getDetails().setText(formatMessage(I18nConstants.MSG_RETIRED, ((PrimitiveType) p.getValue()).asStringValue())); + issues.add(iss); } else if (p.getName().equals("cause")) { try { IssueType it = IssueType.fromCode(((StringType) p.getValue()).getValue()); @@ -1524,15 +1538,18 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte } } } + ValidationResult res = null; if (!ok) { - return new ValidationResult(IssueSeverity.ERROR, message+" (from "+tcc.getClient().getId()+")", err, null).setTxLink(txLog.getLastId()); + res = new ValidationResult(IssueSeverity.ERROR, message+" (from "+tcc.getClient().getId()+")", err, null).setTxLink(txLog.getLastId()); } else if (message != null && !message.equals("No Message returned")) { - return new ValidationResult(IssueSeverity.WARNING, message+" (from "+tcc.getClient().getId()+")", system, version, new ConceptDefinitionComponent().setDisplay(display).setCode(code), display, null).setTxLink(txLog.getLastId()); + res = new ValidationResult(IssueSeverity.WARNING, message+" (from "+tcc.getClient().getId()+")", system, version, new ConceptDefinitionComponent().setDisplay(display).setCode(code), display, null).setTxLink(txLog.getLastId()); } else if (display != null) { - return new ValidationResult(system, version, new ConceptDefinitionComponent().setDisplay(display).setCode(code), display).setTxLink(txLog.getLastId()); + res = new ValidationResult(system, version, new ConceptDefinitionComponent().setDisplay(display).setCode(code), display).setTxLink(txLog.getLastId()); } else { - return new ValidationResult(system, version, new ConceptDefinitionComponent().setCode(code), null).setTxLink(txLog.getLastId()); + res = new ValidationResult(system, version, new ConceptDefinitionComponent().setCode(code), null).setTxLink(txLog.getLastId()); } + res.setIssues(issues ); + return res; } // -------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/IWorkerContext.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/IWorkerContext.java index 1bec15484..49f4b1114 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/IWorkerContext.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/IWorkerContext.java @@ -124,7 +124,7 @@ public interface IWorkerContext { private List issues = new ArrayList<>(); private CodeableConcept codeableConcept; private Set unknownSystems; - + @Override public String toString() { return "ValidationResult [definition=" + definition + ", system=" + system + ", severity=" + severity + ", message=" + message + ", errorClass=" @@ -315,6 +315,14 @@ public interface IWorkerContext { } } + public void setIssues(List issues) { + if (this.issues != null) { + issues.addAll(this.issues); + } + this.issues = issues; + + } + } public class CodingValidationRequest { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Parameters.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Parameters.java index ac42902f3..0d673f2e5 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Parameters.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Parameters.java @@ -1753,6 +1753,14 @@ public String toString() { return true; } return false; + } + + public boolean hasParameterValue(String name, String value) { + for (ParametersParameterComponent p : getParameter()) { + if (p.getName().equals(name) && p.hasValue() && value.equals(p.getValue().primitiveValue())) + return true; + } + return false; } public boolean hasParameter(String name) { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/ValueSet.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/ValueSet.java index 35c721aeb..ccf8106cd 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/ValueSet.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/ValueSet.java @@ -2839,12 +2839,24 @@ public class ValueSet extends MetadataResource { , total, offset, parameter, property, contains); } - public String fhirType() { - return "ValueSet.expansion"; + public boolean hasParameterValue(String name, String value) { + for (ValueSetExpansionParameterComponent p : getParameter()) { + if (name.equals(p.getName()) && p.hasValue() && value.equals(p.getValue().primitiveValue())) { + return true; + } + } + return false; + } - } + public void addParameter(String name, DataType value) { + getParameter().add(new ValueSetExpansionParameterComponent(name).setValue(value)); + } + + public String fhirType() { + return "ValueSet.expansion"; + } - } + } @Block() public static class ValueSetExpansionParameterComponent extends BackboneElement implements IBaseBackboneElement { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/expansion/ValueSetExpander.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/expansion/ValueSetExpander.java index c998a4074..8f5d6e3af 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/expansion/ValueSetExpander.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/expansion/ValueSetExpander.java @@ -124,7 +124,6 @@ public class ValueSetExpander extends ValueSetProcessBase { private static final boolean REPORT_VERSION_ANYWAY = true; - private IWorkerContext context; private ValueSet focus; private List allErrors = new ArrayList<>(); private List requiredSupplements = new ArrayList<>(); @@ -151,9 +150,9 @@ public class ValueSetExpander extends ValueSetProcessBase { private ValueSetExpansionContainsComponent addCode(WorkingContext wc, String system, String code, String display, String dispLang, ValueSetExpansionContainsComponent parent, List designations, Parameters expParams, boolean isAbstract, boolean inactive, String definition, List filters, boolean noInactive, boolean deprecated, List vsProp, - List csProps, List expProps, List csExtList, List vsExtList) throws ETooCostly { + List csProps, List expProps, List csExtList, List vsExtList, ValueSetExpansionComponent exp) throws ETooCostly { - if (filters != null && !filters.isEmpty() && !filterContainsCode(filters, system, code)) + if (filters != null && !filters.isEmpty() && !filterContainsCode(filters, system, code, exp)) return null; if (noInactive && inactive) { return null; @@ -285,10 +284,12 @@ public class ValueSetExpander extends ValueSetProcessBase { return n; } - private boolean filterContainsCode(List filters, String system, String code) { - for (ValueSet vse : filters) + private boolean filterContainsCode(List filters, String system, String code, ValueSetExpansionComponent exp) { + for (ValueSet vse : filters) { + checkCanonical(exp, vse); if (expansionContainsCode(vse.getExpansion().getContains(), system, code)) return true; + } return false; } @@ -312,18 +313,18 @@ public class ValueSetExpander extends ValueSetProcessBase { return null; } - private void addCodeAndDescendents(WorkingContext wc, ValueSetExpansionContainsComponent focus, ValueSetExpansionContainsComponent parent, Parameters expParams, List filters, boolean noInactive, List vsProps, ValueSet vsSrc) throws FHIRException, ETooCostly { + private void addCodeAndDescendents(WorkingContext wc, ValueSetExpansionContainsComponent focus, ValueSetExpansionContainsComponent parent, Parameters expParams, List filters, boolean noInactive, List vsProps, ValueSet vsSrc, ValueSetExpansionComponent exp) throws FHIRException, ETooCostly { focus.checkNoModifiers("Expansion.contains", "expanding"); ValueSetExpansionContainsComponent np = null; for (String code : getCodesForConcept(focus, expParams)) { ValueSetExpansionContainsComponent t = addCode(wc, focus.getSystem(), code, focus.getDisplay(), vsSrc.getLanguage(), parent, - convert(focus.getDesignation()), expParams, focus.getAbstract(), focus.getInactive(), focus.getExtensionString(ToolingExtensions.EXT_DEFINITION), filters, noInactive, false, vsProps, null, focus.getProperty(), null, focus.getExtension()); + convert(focus.getDesignation()), expParams, focus.getAbstract(), focus.getInactive(), focus.getExtensionString(ToolingExtensions.EXT_DEFINITION), filters, noInactive, false, vsProps, null, focus.getProperty(), null, focus.getExtension(), exp); if (np == null) { np = t; } } for (ValueSetExpansionContainsComponent c : focus.getContains()) - addCodeAndDescendents(wc, c, np, expParams, filters, noInactive, vsProps, vsSrc); + addCodeAndDescendents(wc, c, np, expParams, filters, noInactive, vsProps, vsSrc, exp); } private List getCodesForConcept(ValueSetExpansionContainsComponent focus, Parameters expParams) { @@ -349,8 +350,8 @@ public class ValueSetExpander extends ValueSetProcessBase { return list; } - private void addCodeAndDescendents(WorkingContext wc,CodeSystem cs, String system, ConceptDefinitionComponent def, ValueSetExpansionContainsComponent parent, Parameters expParams, List filters, - ConceptDefinitionComponent exclusion, ConceptFilter filterFunc, boolean noInactive, List vsProps, List otherFilters) throws FHIRException, ETooCostly { + private void addCodeAndDescendents(WorkingContext wc, CodeSystem cs, String system, ConceptDefinitionComponent def, ValueSetExpansionContainsComponent parent, Parameters expParams, List filters, + ConceptDefinitionComponent exclusion, ConceptFilter filterFunc, boolean noInactive, List vsProps, List otherFilters, ValueSetExpansionComponent exp) throws FHIRException, ETooCostly { def.checkNoModifiers("Code in Code System", "expanding"); if (exclusion != null) { if (exclusion.getCode().equals(def.getCode())) @@ -362,19 +363,19 @@ public class ValueSetExpander extends ValueSetProcessBase { boolean dep = CodeSystemUtilities.isDeprecated(cs, def, false); if ((includeAbstract || !abs) && filterFunc.includeConcept(cs, def) && passesOtherFilters(otherFilters, cs, def.getCode())) { for (String code : getCodesForConcept(def, expParams)) { - ValueSetExpansionContainsComponent t = addCode(wc, system, code, def.getDisplay(), cs.getLanguage(), parent, def.getDesignation(), expParams, abs, inc, def.getDefinition(), filters, noInactive, dep, vsProps, def.getProperty(), null, def.getExtension(), null); + ValueSetExpansionContainsComponent t = addCode(wc, system, code, def.getDisplay(), cs.getLanguage(), parent, def.getDesignation(), expParams, abs, inc, def.getDefinition(), filters, noInactive, dep, vsProps, def.getProperty(), null, def.getExtension(), null, exp); if (np == null) { np = t; } } } for (ConceptDefinitionComponent c : def.getConcept()) { - addCodeAndDescendents(wc, cs, system, c, np, expParams, filters, exclusion, filterFunc, noInactive, vsProps, otherFilters); + addCodeAndDescendents(wc, cs, system, c, np, expParams, filters, exclusion, filterFunc, noInactive, vsProps, otherFilters, exp); } if (def.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) { List children = (List) def.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK); for (ConceptDefinitionComponent c : children) - addCodeAndDescendents(wc, cs, system, c, np, expParams, filters, exclusion, filterFunc, noInactive, vsProps, otherFilters); + addCodeAndDescendents(wc, cs, system, c, np, expParams, filters, exclusion, filterFunc, noInactive, vsProps, otherFilters, exp); } } @@ -401,7 +402,7 @@ public class ValueSetExpander extends ValueSetProcessBase { - private void addCodes(ValueSetExpansionComponent expand, List params, Parameters expParams, List filters, boolean noInactive, List vsProps, ValueSet vsSrc) throws ETooCostly, FHIRException { + private void addCodes(ValueSetExpansionComponent expand, List params, Parameters expParams, List filters, boolean noInactive, List vsProps, ValueSet vsSrc, ValueSetExpansionComponent exp) throws ETooCostly, FHIRException { if (expand != null) { if (expand.getContains().size() > maxExpansionSize) throw failCostly(context.formatMessage(I18nConstants.VALUESET_TOO_COSTLY, vsSrc.getUrl(), ">" + Integer.toString(expand.getContains().size()))); @@ -410,7 +411,7 @@ public class ValueSetExpander extends ValueSetProcessBase { params.add(p); } - copyImportContains(expand.getContains(), null, expParams, filters, noInactive, vsProps, vsSrc); + copyImportContains(expand.getContains(), null, expParams, filters, noInactive, vsProps, vsSrc, exp); } } @@ -450,8 +451,6 @@ public class ValueSetExpander extends ValueSetProcessBase { throw fail("not done yet - multiple filters"); } - - private void excludeCodes(WorkingContext wc, ValueSetExpansionComponent expand, List params) { for (ValueSetExpansionContainsComponent c : expand.getContains()) { excludeCode(wc, c.getSystem(), c.getCode()); @@ -460,8 +459,9 @@ public class ValueSetExpander extends ValueSetProcessBase { private boolean existsInParams(List params, String name, DataType value) { for (ValueSetExpansionParameterComponent p : params) { - if (p.getName().equals(name) && PrimitiveType.compareDeep(p.getValue(), value, false)) + if (p.getName().equals(name) && PrimitiveType.compareDeep(p.getValue(), value, false)) { return true; + } } return false; } @@ -503,6 +503,7 @@ public class ValueSetExpander extends ValueSetProcessBase { focus.setExpansion(new ValueSet.ValueSetExpansionComponent()); focus.getExpansion().setTimestampElement(DateTimeType.now()); focus.getExpansion().setIdentifier(Factory.createUUID()); + checkCanonical(focus.getExpansion(), focus); for (ParametersParameterComponent p : expParams.getParameter()) { if (Utilities.existsInList(p.getName(), "includeDesignations", "excludeNested", "activeOnly", "offset", "count")) { focus.getExpansion().addParameter().setName(p.getName()).setValue(p.getValue()); @@ -642,6 +643,7 @@ public class ValueSetExpander extends ValueSetProcessBase { throw fail("Unable to find imported value set " + value); } } + checkCanonical(exp, vs); if (noInactive) { expParams = expParams.copy(); expParams.addParameter("activeOnly", true); @@ -714,12 +716,12 @@ public class ValueSetExpander extends ValueSetProcessBase { } } - private void copyImportContains(List list, ValueSetExpansionContainsComponent parent, Parameters expParams, List filter, boolean noInactive, List vsProps, ValueSet vsSrc) throws FHIRException, ETooCostly { + private void copyImportContains(List list, ValueSetExpansionContainsComponent parent, Parameters expParams, List filter, boolean noInactive, List vsProps, ValueSet vsSrc, ValueSetExpansionComponent exp) throws FHIRException, ETooCostly { for (ValueSetExpansionContainsComponent c : list) { c.checkNoModifiers("Imported Expansion in Code System", "expanding"); ValueSetExpansionContainsComponent np = addCode(dwc, c.getSystem(), c.getCode(), c.getDisplay(), vsSrc.getLanguage(), parent, null, expParams, c.getAbstract(), c.getInactive(), c.getExtensionString(ToolingExtensions.EXT_DEFINITION), - filter, noInactive, false, vsProps, null, c.getProperty(), null, c.getExtension()); - copyImportContains(c.getContains(), np, expParams, filter, noInactive, vsProps, vsSrc); + filter, noInactive, false, vsProps, null, c.getProperty(), null, c.getExtension(), exp); + copyImportContains(c.getContains(), np, expParams, filter, noInactive, vsProps, vsSrc, exp); } } @@ -734,9 +736,10 @@ public class ValueSetExpander extends ValueSetProcessBase { if (imports.isEmpty()) // though this is not supposed to be the case return; ValueSet base = imports.get(0); + checkCanonical(exp, base); imports.remove(0); base.checkNoModifiers("Imported ValueSet", "expanding"); - copyImportContains(base.getExpansion().getContains(), null, expParams, imports, noInactive, base.getExpansion().getProperty(), base); + copyImportContains(base.getExpansion().getContains(), null, expParams, imports, noInactive, base.getExpansion().getProperty(), base, exp); } else { CodeSystem cs = context.fetchSupplementedCodeSystem(inc.getSystem()); if (ValueSetUtilities.isServerSide(inc.getSystem()) || (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT))) { @@ -783,7 +786,7 @@ public class ValueSetExpander extends ValueSetProcessBase { } } for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) { - addCodeAndDescendents(dwc, cc, null, expParams, imports, noInactive, vsProps, vs); + addCodeAndDescendents(dwc, cc, null, expParams, imports, noInactive, vsProps, vs, exp); } } @@ -795,6 +798,7 @@ public class ValueSetExpander extends ValueSetProcessBase { else throw failTSE("Unable to find code system " + inc.getSystem().toString()); } + checkCanonical(exp, cs); cs.checkNoModifiers("Code System", "expanding"); if (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT) throw failTSE("Code system " + inc.getSystem().toString() + " is incomplete"); @@ -813,7 +817,7 @@ public class ValueSetExpander extends ValueSetProcessBase { if (inc.getConcept().size() == 0 && inc.getFilter().size() == 0) { // special case - add all the code system for (ConceptDefinitionComponent def : cs.getConcept()) { - addCodeAndDescendents(dwc, cs, inc.getSystem(), def, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), null); + addCodeAndDescendents(dwc, cs, inc.getSystem(), def, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), null, exp); } if (cs.getContent() == CodeSystemContentMode.FRAGMENT) { addFragmentWarning(exp, cs); @@ -846,7 +850,7 @@ public class ValueSetExpander extends ValueSetProcessBase { isAbstract = CodeSystemUtilities.isNotSelectable(cs, def); } addCode(dwc, inc.getSystem(), c.getCode(), !Utilities.noString(c.getDisplay()) ? c.getDisplay() : def == null ? null : def.getDisplay(), c.hasDisplay() ? vsSrc.getLanguage() : cs.getLanguage(), null, mergeDesignations(def, convertDesignations(c.getDesignation())), - expParams, isAbstract, inactive, def == null ? null : def.getDefinition(), imports, noInactive, false, exp.getProperty(), def != null ? def.getProperty() : null, null, def == null ? null : def.getExtension(), c.getExtension()); + expParams, isAbstract, inactive, def == null ? null : def.getDefinition(), imports, noInactive, false, exp.getProperty(), def != null ? def.getProperty() : null, null, def == null ? null : def.getExtension(), c.getExtension(), exp); } } if (inc.getFilter().size() > 0) { @@ -876,14 +880,14 @@ public class ValueSetExpander extends ValueSetProcessBase { ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue()); if (def == null) throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'"); - addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters); + addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp); } else if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.ISNOTA) { // special: all codes in the target code system that are not under the value ConceptDefinitionComponent defEx = getConceptForCode(cs.getConcept(), fc.getValue()); if (defEx == null) throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'"); for (ConceptDefinitionComponent def : cs.getConcept()) { - addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, defEx, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters); + addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, defEx, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp); } } else if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.DESCENDENTOF) { // special: all codes in the target code system under the value @@ -891,11 +895,11 @@ public class ValueSetExpander extends ValueSetProcessBase { if (def == null) throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'"); for (ConceptDefinitionComponent c : def.getConcept()) - addCodeAndDescendents(wc, cs, inc.getSystem(), c, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters); + addCodeAndDescendents(wc, cs, inc.getSystem(), c, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp); if (def.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) { List children = (List) def.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK); for (ConceptDefinitionComponent c : children) - addCodeAndDescendents(wc, cs, inc.getSystem(), c, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters); + addCodeAndDescendents(wc, cs, inc.getSystem(), c, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp); } } else if ("display".equals(fc.getProperty()) && fc.getOp() == FilterOperator.EQUAL) { @@ -907,18 +911,18 @@ public class ValueSetExpander extends ValueSetProcessBase { if (def.getDisplay().contains(fc.getValue()) && passesOtherFilters(filters, cs, def.getCode())) { for (String code : getCodesForConcept(def, expParams)) { ValueSetExpansionContainsComponent t = addCode(wc, inc.getSystem(), code, def.getDisplay(), cs.getLanguage(), null, def.getDesignation(), expParams, CodeSystemUtilities.isNotSelectable(cs, def), CodeSystemUtilities.isInactive(cs, def), - def.getDefinition(), imports, noInactive, false, exp.getProperty(), def.getProperty(), null, def.getExtension(), null); + def.getDefinition(), imports, noInactive, false, exp.getProperty(), def.getProperty(), null, def.getExtension(), null, exp); } } } } } else if (isDefinedProperty(cs, fc.getProperty())) { for (ConceptDefinitionComponent def : cs.getConcept()) { - addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, new PropertyFilter(allErrors, fc, getPropertyDefinition(cs, fc.getProperty())), noInactive, exp.getProperty(), filters); + addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, new PropertyFilter(allErrors, fc, getPropertyDefinition(cs, fc.getProperty())), noInactive, exp.getProperty(), filters, exp); } } else if ("code".equals(fc.getProperty()) && fc.getOp() == FilterOperator.REGEX) { for (ConceptDefinitionComponent def : cs.getConcept()) { - addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, new RegexFilter(allErrors, fc.getValue()), noInactive, exp.getProperty(), filters); + addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, new RegexFilter(allErrors, fc.getValue()), noInactive, exp.getProperty(), filters, exp); } } else { throw fail("Filter by property[" + fc.getProperty() + "] and op[" + fc.getOp() + "] is not supported yet"); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/ValueSetProcessBase.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/ValueSetProcessBase.java index 30de48170..9db2b9c3e 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/ValueSetProcessBase.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/ValueSetProcessBase.java @@ -1,20 +1,31 @@ package org.hl7.fhir.r5.terminologies.utilities; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.model.BooleanType; +import org.hl7.fhir.r5.model.CanonicalResource; +import org.hl7.fhir.r5.model.DataType; +import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; +import org.hl7.fhir.r5.model.OperationOutcome.IssueType; +import org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent; import org.hl7.fhir.r5.model.Extension; import org.hl7.fhir.r5.model.Parameters; -import org.hl7.fhir.r5.model.ValueSet; -import org.hl7.fhir.r5.model.BooleanType; -import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent; -import org.hl7.fhir.r5.model.DataType; import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent; -import org.hl7.fhir.r5.model.PrimitiveType; +import org.hl7.fhir.r5.model.UriType; +import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionComponent; import org.hl7.fhir.r5.utils.ToolingExtensions; +import org.hl7.fhir.utilities.StandardsStatus; import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.i18n.I18nConstants; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; public class ValueSetProcessBase { + protected IWorkerContext context; + public static class AlternateCodesProcessingRules { private boolean all; private List uses = new ArrayList<>(); @@ -77,6 +88,84 @@ public class ValueSetProcessBase { } } + + protected List makeIssue(IssueSeverity level, IssueType type, String location, String message) { + OperationOutcomeIssueComponent result = new OperationOutcomeIssueComponent(); + switch (level) { + case ERROR: + result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.ERROR); + break; + case FATAL: + result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.FATAL); + break; + case INFORMATION: + result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.INFORMATION); + break; + case WARNING: + result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.WARNING); + break; + } + result.setCode(type); + result.addLocation(location); + result.getDetails().setText(message); + ArrayList list = new ArrayList<>(); + list.add(result); + return list; + } + + public void checkCanonical(List issues, String path, CanonicalResource resource) { + StandardsStatus standardsStatus = ToolingExtensions.getStandardsStatus(resource); + if (standardsStatus == StandardsStatus.DEPRECATED) { + addToIssues(issues, makeStatusIssue(path, "deprecated", I18nConstants.MSG_DEPRECATED, resource)); + } else if (standardsStatus == StandardsStatus.WITHDRAWN) { + addToIssues(issues, makeStatusIssue(path, "withdrawn", I18nConstants.MSG_WITHDRAWN, resource)); + } else if (resource.getStatus() == PublicationStatus.RETIRED) { + addToIssues(issues, makeStatusIssue(path, "retired", I18nConstants.MSG_RETIRED, resource)); + } + } + + private List makeStatusIssue(String path, String id, String msg, CanonicalResource resource) { + List iss = makeIssue(IssueSeverity.INFORMATION, IssueType.EXPIRED, path, context.formatMessage(msg, resource.getVersionedUrl())); + + // this is a testing hack - see TerminologyServiceTests + iss.get(0).setUserData("status-msg-name", "warning-"+id); + iss.get(0).setUserData("status-msg-value", new UriType(resource.getVersionedUrl())); + + return iss; + } + + private void addToIssues(List issues, List toAdd) { + for (OperationOutcomeIssueComponent t : toAdd) { + boolean found = false; + for (OperationOutcomeIssueComponent i : issues) { + if (i.getSeverity() == t.getSeverity() && i.getCode() == t.getCode() && i.getDetails().getText().equals(t.getDetails().getText())) { // ignore location + found = true; + } + } + if (!found) { + issues.add(t); + } + } + } + + public void checkCanonical(ValueSetExpansionComponent params, CanonicalResource resource) { + StandardsStatus standardsStatus = ToolingExtensions.getStandardsStatus(resource); + if (standardsStatus == StandardsStatus.DEPRECATED) { + if (!params.hasParameterValue("warning-deprecated", resource.getVersionedUrl())) { + params.addParameter("warning-deprecated", new UriType(resource.getVersionedUrl())); + } + } else if (standardsStatus == StandardsStatus.WITHDRAWN) { + if (!params.hasParameterValue("warning-withdrawn", resource.getVersionedUrl())) { + params.addParameter("warning-withdrawn", new UriType(resource.getVersionedUrl())); + } + } else if (resource.getStatus() == PublicationStatus.RETIRED) { + if (!params.hasParameterValue("warning-retired", resource.getVersionedUrl())) { + params.addParameter("warning-retired", new UriType(resource.getVersionedUrl())); + } + } + } + + protected AlternateCodesProcessingRules altCodeParams = new AlternateCodesProcessingRules(false); protected AlternateCodesProcessingRules allAltCodes = new AlternateCodesProcessingRules(true); } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/validation/ValueSetValidator.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/validation/ValueSetValidator.java index 5fe76d5c5..2725724dd 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/validation/ValueSetValidator.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/validation/ValueSetValidator.java @@ -97,7 +97,6 @@ import com.google.j2objc.annotations.ReflectionSupport.Level; public class ValueSetValidator extends ValueSetProcessBase { private ValueSet valueset; - private IWorkerContext context; private Map inner = new HashMap<>(); private ValidationOptions options; private ValidationContextCarrier localContext; @@ -229,7 +228,7 @@ public class ValueSetValidator extends ValueSetProcessBase { for (Coding c : code.getCoding()) { b.append(c.getSystem()+(c.hasVersion() ? "|"+c.getVersion() : "")+"#"+c.getCode()); - Boolean ok = codeInValueSet(c.getSystem(), c.getVersion(), c.getCode(), info); + Boolean ok = codeInValueSet(path, c.getSystem(), c.getVersion(), c.getCode(), info); if (ok == null && result != null && result == false) { result = null; } else if (ok != null && ok) { @@ -338,30 +337,6 @@ public class ValueSetValidator extends ValueSetProcessBase { return versionTest == null && VersionUtilities.versionsMatch(versionTest, versionActual); } - private List makeIssue(IssueSeverity level, IssueType type, String location, String message) { - OperationOutcomeIssueComponent result = new OperationOutcomeIssueComponent(); - switch (level) { - case ERROR: - result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.ERROR); - break; - case FATAL: - result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.FATAL); - break; - case INFORMATION: - result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.INFORMATION); - break; - case WARNING: - result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.WARNING); - break; - } - result.setCode(type); - result.addLocation(location); - result.getDetails().setText(message); - ArrayList list = new ArrayList<>(); - list.add(result); - return list; - } - public ValidationResult validateCode(Coding code) throws FHIRException { return validateCode("Coding", code); } @@ -375,6 +350,7 @@ public class ValueSetValidator extends ValueSetProcessBase { boolean inInclude = false; List issues = new ArrayList<>(); VersionInfo vi = new VersionInfo(this); + checkCanonical(issues, path, valueset); String system = code.hasSystem() ? code.getSystem() : getValueSetSystemOrNull(); if (options.getValueSetMode() != ValueSetMode.CHECK_MEMERSHIP_ONLY) { @@ -434,6 +410,8 @@ public class ValueSetValidator extends ValueSetProcessBase { } } } + } else { + checkCanonical(issues, path, cs); } if (cs != null && cs.hasSupplements()) { String msg = context.formatMessage(I18nConstants.CODESYSTEM_CS_NO_SUPPLEMENT, cs.getUrl()); @@ -462,6 +440,7 @@ public class ValueSetValidator extends ValueSetProcessBase { throw new FHIRException("Unable to evaluate based on empty code system"); } res = validateCode(path, code, cs, null); + res.setIssues(issues); } else if (cs == null && valueset.hasExpansion() && inExpansion) { // 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()); @@ -486,7 +465,7 @@ public class ValueSetValidator extends ValueSetProcessBase { // then, if we have a value set, we check it's in the value set if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) { if ((res==null || res.isOk())) { - Boolean ok = codeInValueSet(system, wv, code.getCode(), info); + Boolean ok = codeInValueSet(path, system, wv, code.getCode(), info); if (ok == null || !ok) { if (res == null) { res = new ValidationResult((IssueSeverity) null, null, info.getIssues()); @@ -982,14 +961,11 @@ public class ValueSetValidator extends ValueSetProcessBase { return true; } - 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; } + checkCanonical(info.getIssues(), path, valueset); Boolean result = false; VersionInfo vi = new VersionInfo(this); @@ -1029,15 +1005,15 @@ public class ValueSetValidator extends ValueSetProcessBase { if (isValueSetUnionImports()) { ok = false; for (UriType uri : vsi.getValueSet()) { - if (inImport(uri.getValue(), system, version, code)) { + if (inImport(path, uri.getValue(), system, version, code, info)) { return true; } } } else { - ok = inImport(vsi.getValueSet().get(0).getValue(), system, version, code); + ok = inImport(path, vsi.getValueSet().get(0).getValue(), system, version, code, info); for (int i = 1; i < vsi.getValueSet().size(); i++) { UriType uri = vsi.getValueSet().get(i); - ok = ok && inImport(uri.getValue(), system, version, code); + ok = ok && inImport(path, uri.getValue(), system, version, code, info); } } } @@ -1097,6 +1073,7 @@ public class ValueSetValidator extends ValueSetProcessBase { return null; } } else { + checkCanonical(info.getIssues(), path, cs); if (vsi.hasFilter()) { ok = true; for (ConceptSetFilterComponent f : vsi.getFilter()) { @@ -1231,12 +1208,12 @@ public class ValueSetValidator extends ValueSetProcessBase { return vsc; } - private boolean inImport(String uri, String system, String version, String code) throws FHIRException { + private boolean inImport(String path, String uri, String system, String version, String code, ValidationProcessInfo info) throws FHIRException { ValueSetValidator vs = getVs(uri); if (vs == null) { return false; } else { - Boolean ok = vs.codeInValueSet(system, version, code, null); + Boolean ok = vs.codeInValueSet(path, system, version, code, info); return ok != null && ok; } } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/StandardsStatus.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/StandardsStatus.java index e39aa5caf..059fa0d0c 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/StandardsStatus.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/StandardsStatus.java @@ -35,7 +35,7 @@ import org.hl7.fhir.exceptions.FHIRException; public enum StandardsStatus { - EXTERNAL, INFORMATIVE, DRAFT, TRIAL_USE, DEPRECATED, NORMATIVE; + EXTERNAL, INFORMATIVE, DRAFT, TRIAL_USE, DEPRECATED, NORMATIVE, WITHDRAWN; public String toDisplay() { switch (this) { @@ -51,6 +51,8 @@ public enum StandardsStatus { return "External"; case DEPRECATED: return "Deprecated"; + case WITHDRAWN: + return "Withdrawn"; } return "?"; } @@ -66,9 +68,11 @@ public enum StandardsStatus { case INFORMATIVE: return "informative"; case DEPRECATED: - return "deprecated"; + return "deprecated"; case EXTERNAL: return "external"; + case WITHDRAWN: + return "Withdrawn"; } return "?"; } @@ -93,6 +97,8 @@ public enum StandardsStatus { return EXTERNAL; if (value.equalsIgnoreCase("DEPRECATED")) return DEPRECATED; + if (value.equalsIgnoreCase("WITHDRAWN")) + return WITHDRAWN; throw new FHIRException("Incorrect Standards Status '"+value+"'"); } @@ -110,6 +116,8 @@ public enum StandardsStatus { return "XD"; case EXTERNAL: return "X"; + case WITHDRAWN: + return "W"; } return "?"; } @@ -125,6 +133,7 @@ public enum StandardsStatus { case INFORMATIVE: return "#ffffe6"; case DEPRECATED: + case WITHDRAWN: return "#ffcccc"; case EXTERNAL: return "#e6ffff"; @@ -143,6 +152,7 @@ public enum StandardsStatus { case INFORMATIVE: return "#ffffec"; case DEPRECATED: + case WITHDRAWN: return "#ffcccc"; case EXTERNAL: return "#ecffff"; @@ -159,6 +169,8 @@ public enum StandardsStatus { return (tgtSS == NORMATIVE || tgtSS == EXTERNAL ); if (this == DEPRECATED) return (tgtSS == DEPRECATED ); + if (this == WITHDRAWN) + return (tgtSS == WITHDRAWN ); return false; } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java index c60c6d763..29610c9f8 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java @@ -950,6 +950,9 @@ public class I18nConstants { public static final String FHIRPATH_OFTYPE_IMPOSSIBLE = "FHIRPATH_OFTYPE_IMPOSSIBLE"; public static final String ED_SEARCH_EXPRESSION_ERROR = "ED_SEARCH_EXPRESSION_ERROR"; public static final String SD_EXTENSION_URL_MISMATCH = "SD_EXTENSION_URL_MISMATCH"; + public static final String MSG_DEPRECATED = "MSG_DEPRECATED"; + public static final String MSG_WITHDRAWN = "MSG_WITHDRAWN"; + public static final String MSG_RETIRED = "MSG_RETIRED"; diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 5aefff864..fab96dfe2 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -1005,4 +1005,7 @@ FHIRPATH_COLLECTION_STATUS_OPERATION_LEFT = The left side is inherently a collec FHIRPATH_COLLECTION_STATUS_OPERATION_RIGHT = The right side is inherently a collection, and so this expression ''{0}'' may fail or return false if there is more than one item in the content being evaluated FHIRPATH_OFTYPE_IMPOSSIBLE = The type specified in ofType is {1} which is not a possible candidate for the existing types ({0}) in the expression {2}. Check the paths and types to be sure this is what is intended ED_SEARCH_EXPRESSION_ERROR = Error in search expression ''{0}'': {1} -SD_EXTENSION_URL_MISMATCH = The fixed value for the extension URL is {1} which doesn''t match the canonical URL {0} \ No newline at end of file +SD_EXTENSION_URL_MISMATCH = The fixed value for the extension URL is {1} which doesn''t match the canonical URL {0} +MSG_DEPRECATED = Reference to deprecated item {0} +MSG_WITHDRAWN = Reference to withdrawn item {0} +MSG_RETIRED = Reference to retired item {0} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/TxTesterSorters.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/TxTesterSorters.java index e896a4523..30e42ce36 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/TxTesterSorters.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/TxTesterSorters.java @@ -3,7 +3,11 @@ package org.hl7.fhir.validation.special; import java.util.Collections; import java.util.Comparator; +import org.hl7.fhir.ParametersParameter; +import org.hl7.fhir.r5.model.Base; import org.hl7.fhir.r5.model.Extension; +import org.hl7.fhir.r5.model.OperationOutcome; +import org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent; import org.hl7.fhir.r5.model.Parameters; import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r5.model.ValueSet; @@ -16,8 +20,14 @@ import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionPropertyComponent; public class TxTesterSorters { + public static void sortParameters(Parameters po) { Collections.sort(po.getParameter(), new TxTesterSorters.ParameterSorter()); + for (ParametersParameterComponent p : po.getParameter()) { + if (p.getResource() != null && p.getResource() instanceof OperationOutcome) { + Collections.sort(((OperationOutcome) p.getResource()).getIssue(), new TxTesterSorters.OperationIssueSorter()); + } + } } @@ -43,7 +53,24 @@ public class TxTesterSorters { } - + + public static class OperationIssueSorter implements Comparator { + + @Override + public int compare(OperationOutcomeIssueComponent o1, OperationOutcomeIssueComponent o2) { + if (o1.hasSeverity() && o2.hasSeverity() && o1.getSeverity() != o2.getSeverity()) { + return o1.getSeverity().compareTo(o2.getSeverity()); + } + if (o1.hasCode() && o2.hasCode() && o1.getCode() != o2.getCode()) { + return o1.getCode().compareTo(o2.getCode()); + } + if (o1.getDetails().hasText() && o2.getDetails().hasText() && !o1.getDetails().getText().equals(o2.getDetails().getText())) { + return o1.getDetails().getText().compareTo(o2.getDetails().getText()); + } + return 0; + } + } + public static class DesignationSorter implements Comparator { @Override