From b79d71f94738bb6e79c0a193c0a39efb44301083 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Mon, 13 Dec 2021 18:36:56 +1100 Subject: [PATCH 1/4] better version specific resolution of special canonical resources --- .../org/hl7/fhir/utilities/SIDUtilities.java | 38 +++++- .../instance/InstanceValidator.java | 125 +++++++++++------- 2 files changed, 108 insertions(+), 55 deletions(-) diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/SIDUtilities.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/SIDUtilities.java index 69664a879..7f7d11101 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/SIDUtilities.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/SIDUtilities.java @@ -10,11 +10,9 @@ public class SIDUtilities { public static List codeSystemList() { List codeSystems = new ArrayList<>(); codeSystems.add("http://hl7.org/fhir/sid/ndc"); - codeSystems.add("http://hl7.org/fhir/sid/icd-10"); codeSystems.add("http://hl7.org/fhir/sid/icpc2"); codeSystems.add("http://hl7.org/fhir/sid/icd-9"); codeSystems.add("http://hl7.org/fhir/sid/icd-10"); - codeSystems.add("http://hl7.org/fhir/sid/icpc2"); codeSystems.add("http://hl7.org/fhir/sid/cvx"); codeSystems.add("http://hl7.org/fhir/sid/srt"); codeSystems.add("http://hl7.org/fhir/sid/icd-10-vn"); @@ -55,8 +53,38 @@ public class SIDUtilities { allSystems.addAll(idSystemList()); return allSystems; } - - - + public static boolean isInvalidVersion(String u, String v) { + if (v == null) { + return false; + } else { + if (idSystemList().contains(u)) { + return true; + } else { + switch (u) { + case "http://hl7.org/fhir/sid/ndc": + return v.matches("[\\d]{8}"); + case "http://hl7.org/fhir/sid/icpc2": + return false; + case "http://hl7.org/fhir/sid/icd-10": + return false; + case "http://hl7.org/fhir/sid/icd-9": + return false; + case "http://hl7.org/fhir/sid/cvx": + return v.matches("[\\d]{8}"); + case "http://hl7.org/fhir/sid/srt": + return false; + case "http://hl7.org/fhir/sid/icd-10-vn": + return false; + case "http://hl7.org/fhir/sid/icd-10-cm": + return false; + case "http://hl7.org/fhir/sid/icd-9-cm": + return false; + default: + return true; + } + } + } + } + } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java index 897303766..a2b3820d9 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java @@ -2114,52 +2114,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat rule(errors, IssueType.INVALID, e.line(), e.col(), path, Utilities.isAbsoluteUrl(url), node.isContained() ? I18nConstants.TYPE_SPECIFIC_CHECKS_CANONICAL_CONTAINED : I18nConstants.TYPE_SPECIFIC_CHECKS_CANONICAL_ABSOLUTE, url); } else { - // now, do we check the URI target? - if (fetcher != null && !type.equals("uuid")) { - boolean found; - try { - found = isDefinitionURL(url) || (allowExamples && (url.contains("example.org") || url.contains("acme.com")) || url.contains("acme.org")) || (url.startsWith("http://hl7.org/fhir/tools")) || - SpecialExtensions.isKnownExtension(url) || isXverUrl(url) || fetcher.resolveURL(this, hostContext, path, url, type); - } catch (IOException e1) { - found = false; - } - if (!found) { - if (type.equals("canonical")) { - ReferenceValidationPolicy rp = policyAdvisor == null ? ReferenceValidationPolicy.CHECK_VALID : policyAdvisor.policyForReference(this, hostContext, path, url); - if (rp == ReferenceValidationPolicy.CHECK_EXISTS || rp == ReferenceValidationPolicy.CHECK_EXISTS_AND_TYPE) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE, url); - } else { - hint(errors, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE, url); - } - } else { - if (url.contains("hl7.org") || url.contains("fhir.org")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_URL_RESOLVE, url); - } else if (url.contains("example.org") || url.contains("acme.com")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_URL_EXAMPLE, url); - } else { - warning(errors, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_URL_RESOLVE, url); - } - } - } else { - if (type.equals("canonical")) { - ReferenceValidationPolicy rp = policyAdvisor == null ? ReferenceValidationPolicy.CHECK_VALID : policyAdvisor.policyForReference(this, hostContext, path, url); - if (rp == ReferenceValidationPolicy.CHECK_EXISTS_AND_TYPE || rp == ReferenceValidationPolicy.CHECK_TYPE_IF_EXISTS || rp == ReferenceValidationPolicy.CHECK_VALID) { - try { - Resource r = fetcher.fetchCanonicalResource(this, url); - if (r == null) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE, url); - } else if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, isCorrectCanonicalType(r, context), I18nConstants.TYPE_SPECIFIC_CHECKS_DT_CANONICAL_TYPE, url, r.fhirType(), listExpectedCanonicalTypes(context))) { - if (rp == ReferenceValidationPolicy.CHECK_VALID) { - // todo.... - } - } - } catch (Exception ex) { - // won't happen - } - } - } - } - } + validateReference(hostContext, errors, path, type, context, e, url); } } if (type.equals(ID) && !"Resource.id".equals(context.getBase().getPath())) { @@ -2323,6 +2278,67 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat // for nothing to check } + public void validateReference(ValidatorHostContext hostContext, List errors, String path, String type, ElementDefinition context, Element e, String url) { + // now, do we check the URI target? + if (fetcher != null && !type.equals("uuid")) { + boolean found; + try { + found = isDefinitionURL(url) || (allowExamples && (url.contains("example.org") || url.contains("acme.com")) || url.contains("acme.org")) || (url.startsWith("http://hl7.org/fhir/tools")) || + SpecialExtensions.isKnownExtension(url) || isXverUrl(url); + if (!found) { + found = fetcher.resolveURL(this, hostContext, path, url, type); + } + } catch (IOException e1) { + found = false; + } + if (!found) { + if (type.equals("canonical")) { + ReferenceValidationPolicy rp = policyAdvisor == null ? ReferenceValidationPolicy.CHECK_VALID : policyAdvisor.policyForReference(this, hostContext, path, url); + if (rp == ReferenceValidationPolicy.CHECK_EXISTS || rp == ReferenceValidationPolicy.CHECK_EXISTS_AND_TYPE) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE, url); + } else { + hint(errors, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE, url); + } + } else { + if (url.contains("hl7.org") || url.contains("fhir.org")) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_URL_RESOLVE, url); + } else if (url.contains("example.org") || url.contains("acme.com")) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_URL_EXAMPLE, url); + } else { + warning(errors, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_URL_RESOLVE, url); + } + } + } else { + if (type.equals("canonical")) { + ReferenceValidationPolicy rp = policyAdvisor == null ? ReferenceValidationPolicy.CHECK_VALID : policyAdvisor.policyForReference(this, hostContext, path, url); + if (rp == ReferenceValidationPolicy.CHECK_EXISTS_AND_TYPE || rp == ReferenceValidationPolicy.CHECK_TYPE_IF_EXISTS || rp == ReferenceValidationPolicy.CHECK_VALID) { + try { + Resource r = null; + if (url.startsWith("#")) { + r = loadContainedResource(errors, path, hostContext.getRootResource(), url.substring(1), Resource.class); + } + if (r == null) { + fetcher.fetchCanonicalResource(this, url); + } + if (r == null) { + r = this.context.fetchResource(Resource.class, url); + } + if (r == null) { + warning(errors, IssueType.INVALID, e.line(), e.col(), path, rp != ReferenceValidationPolicy.CHECK_VALID, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE_NC, url); + } else if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, isCorrectCanonicalType(r, context), I18nConstants.TYPE_SPECIFIC_CHECKS_DT_CANONICAL_TYPE, url, r.fhirType(), listExpectedCanonicalTypes(context))) { + if (rp == ReferenceValidationPolicy.CHECK_VALID) { + // todo.... + } + } + } catch (Exception ex) { + // won't happen + } + } + } + } + } + } + private List listExpectedCanonicalTypes(ElementDefinition context) { List res = new ArrayList<>(); TypeRefComponent tr = context.getType("canonical"); @@ -2354,11 +2370,20 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat private boolean isCorrectCanonicalType(Resource r, CanonicalType p) { String url = p.getValue(); - if (url != null && url.startsWith("http://hl7.org/fhir/StructureDefinition/")) { - url = url.substring("http://hl7.org/fhir/StructureDefinition/".length()); - return Utilities.existsInList(url, "Resource", "CanonicalResource") || url.equals(r.fhirType()); + String t = null; + if (url.startsWith("http://hl7.org/fhir/StructureDefinition/")) { + t = url.substring("http://hl7.org/fhir/StructureDefinition/".length()); + } else { + StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); + if (sd != null) { + t = sd.getType(); + } + } + if (t == null ) { + return false; + } else { + return Utilities.existsInList(t, "Resource", "CanonicalResource") || t.equals(r.fhirType()); } - return false; } private boolean isCanonicalURLElement(Element e) { From 81249998c2e33e8fca8912a31f96b1b8580cbe78 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Mon, 13 Dec 2021 18:37:16 +1100 Subject: [PATCH 2/4] fix up r4b canonical list --- .../hl7/fhir/utilities/VersionUtilities.java | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/VersionUtilities.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/VersionUtilities.java index b9edecf17..6582909c3 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/VersionUtilities.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/VersionUtilities.java @@ -397,7 +397,6 @@ public class VersionUtilities { } if (isR4Ver(version)) { - res.add("CodeSystem"); res.add("ActivityDefinition"); res.add("CapabilityStatement"); @@ -429,6 +428,38 @@ public class VersionUtilities { res.add("TestScript"); res.add("ValueSet"); } + if (isR4BVer(version)) { + res.add("ActivityDefinition"); + res.add("CapabilityStatement"); + res.add("ChargeItemDefinition"); + res.add("Citation"); + res.add("CodeSystem"); + res.add("CompartmentDefinition"); + res.add("ConceptMap"); + res.add("EventDefinition"); + res.add("Evidence"); + res.add("EvidenceReport"); + res.add("EvidenceVariable"); + res.add("ExampleScenario"); + res.add("GraphDefinition"); + res.add("ImplementationGuide"); + res.add("Library"); + res.add("Measure"); + res.add("MessageDefinition"); + res.add("NamingSystem"); + res.add("OperationDefinition"); + res.add("PlanDefinition"); + res.add("Questionnaire"); + res.add("ResearchDefinition"); + res.add("ResearchElementDefinition"); + res.add("SearchParameter"); + res.add("StructureDefinition"); + res.add("StructureMap"); + res.add("SubscriptionTopic"); + res.add("TerminologyCapabilities"); + res.add("TestScript"); + res.add("ValueSet"); + } if (isR5Ver(version) || "current".equals(version)) { From 65e65d3e7b76ae8abe14e3aa5bcac7a2e9ff40cc Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Mon, 13 Dec 2021 18:39:44 +1100 Subject: [PATCH 3/4] fix bug in client caching for unidentified value sets + look for code system definitions in local context + fix path error in Questionnaire Response validation --- .../fhir/r5/context/BaseWorkerContext.java | 15 +++- .../hl7/fhir/r5/context/IWorkerContext.java | 9 +- .../terminologies/ValueSetCheckerSimple.java | 69 ++++++++++++++-- .../validation/ValidationContextCarrier.java | 81 ++++++++++++++++++ .../fhir/utilities/i18n/I18nConstants.java | 1 + .../src/main/resources/Messages.properties | 1 + .../hl7/fhir/validation/BaseValidator.java | 9 +- .../instance/type/QuestionnaireValidator.java | 82 ++++++++++++++----- 8 files changed, 229 insertions(+), 38 deletions(-) create mode 100644 org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/validation/ValidationContextCarrier.java 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 44be71cf6..7e073a55a 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 @@ -111,6 +111,7 @@ import org.hl7.fhir.r5.terminologies.ValueSetExpander.TerminologyServiceErrorCla import org.hl7.fhir.r5.terminologies.ValueSetExpander.ValueSetExpansionOutcome; import org.hl7.fhir.r5.terminologies.ValueSetExpanderSimple; import org.hl7.fhir.r5.utils.ToolingExtensions; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; import org.hl7.fhir.utilities.OIDUtils; import org.hl7.fhir.utilities.TimeTracker; import org.hl7.fhir.utilities.ToolingClientLogger; @@ -927,6 +928,12 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte @Override public ValidationResult validateCode(ValidationOptions options, Coding code, ValueSet vs) { + ValidationContextCarrier ctxt = new ValidationContextCarrier(); + return validateCode(options, code, vs, ctxt); + } + + @Override + public ValidationResult validateCode(ValidationOptions options, Coding code, ValueSet vs, ValidationContextCarrier ctxt) { if (options == null) { options = ValidationOptions.defaults(); } @@ -946,7 +953,7 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte if (options.isUseClient()) { // ok, first we try to validate locally try { - ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, this); + ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, this, ctxt); if (!vsc.isServerSide(code.getSystem())) { res = vsc.validateCode(code); if (txCache != null) { @@ -1066,13 +1073,15 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte } } if (vs != null) { - if (isTxCaching && cacheId != null && cached.contains(vs.getUrl()+"|"+vs.getVersion())) { + if (isTxCaching && cacheId != null && vs.getUrl() != null && cached.contains(vs.getUrl()+"|"+vs.getVersion())) { pin.addParameter().setName("url").setValue(new UriType(vs.getUrl()+(vs.hasVersion() ? "|"+vs.getVersion() : ""))); } else if (options.getVsAsUrl()){ pin.addParameter().setName("url").setValue(new StringType(vs.getUrl())); } else { pin.addParameter().setName("valueSet").setResource(vs); - cached.add(vs.getUrl()+"|"+vs.getVersion()); + if (vs.getUrl() != null) { + cached.add(vs.getUrl()+"|"+vs.getVersion()); + } } cache = true; addDependentResources(pin, vs); 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 d7ca6e3ac..ed0afccfc 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 @@ -45,6 +45,7 @@ import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.TerminologyServiceException; import org.hl7.fhir.r5.context.TerminologyCache.CacheToken; +import org.hl7.fhir.r5.elementmodel.Element; import org.hl7.fhir.r5.formats.IParser; import org.hl7.fhir.r5.formats.ParserType; import org.hl7.fhir.r5.model.Bundle; @@ -64,11 +65,13 @@ import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; import org.hl7.fhir.r5.terminologies.ValueSetExpander.TerminologyServiceErrorClass; import org.hl7.fhir.r5.terminologies.ValueSetExpander.ValueSetExpansionOutcome; import org.hl7.fhir.r5.utils.validation.IResourceValidator; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; import org.hl7.fhir.utilities.TimeTracker; import org.hl7.fhir.utilities.TranslationServices; import org.hl7.fhir.utilities.npm.BasePackageCacheManager; import org.hl7.fhir.utilities.npm.NpmPackage; import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; +import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.validation.ValidationOptions; import com.google.gson.JsonSyntaxException; @@ -193,13 +196,14 @@ public interface IWorkerContext { } } + public interface ICanonicalResourceLocator { void findResource(Object caller, String url); // if it can be found, put it in the context } public interface IContextResourceLoader { /** - * @return List of the resource types that shoud be loaded + * @return List of the resource types that should be loaded */ String[] getTypes(); @@ -244,7 +248,6 @@ public interface IWorkerContext { IContextResourceLoader getNewLoader(NpmPackage npm) throws JsonSyntaxException, IOException; } - /** * Get the versions of the definitions loaded in context * @return @@ -745,6 +748,8 @@ public interface IWorkerContext { * @return */ public ValidationResult validateCode(ValidationOptions options, Coding code, ValueSet vs); + + public ValidationResult validateCode(ValidationOptions options, Coding code, ValueSet vs, ValidationContextCarrier ctxt); public void validateCodeBatch(ValidationOptions options, List codes, ValueSet vs); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetCheckerSimple.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetCheckerSimple.java index 6a906bb49..706e70e7a 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetCheckerSimple.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetCheckerSimple.java @@ -56,6 +56,9 @@ import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent; import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; import org.hl7.fhir.r5.terminologies.ValueSetExpander.TerminologyServiceErrorClass; +import org.hl7.fhir.r5.utils.ToolingExtensions; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy; import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.i18n.I18nConstants; @@ -69,12 +72,52 @@ public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChe private IWorkerContext context; private Map inner = new HashMap<>(); private ValidationOptions options; + private ValidationContextCarrier localContext; + private List localSystems = new ArrayList<>(); public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context) { this.valueset = source; this.context = context; this.options = options; } + + public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context, ValidationContextCarrier ctxt) { + this.valueset = source; + this.context = context; + this.options = options; + this.localContext = ctxt; + analyseValueSet(); + } + + private void analyseValueSet() { + if (localContext != null) { + if (valueset != null) { + for (ConceptSetComponent i : valueset.getCompose().getInclude()) { + analyseComponent(i); + } + for (ConceptSetComponent i : valueset.getCompose().getExclude()) { + analyseComponent(i); + } + } + } + } + + private void analyseComponent(ConceptSetComponent i) { + if (i.getSystemElement().hasExtension(ToolingExtensions.EXT_VALUESET_SYSTEM)) { + String ref = i.getSystemElement().getExtensionString(ToolingExtensions.EXT_VALUESET_SYSTEM); + if (ref.startsWith("#")) { + String id = ref.substring(1); + for (ValidationContextResourceProxy t : localContext.getResources()) { + CodeSystem cs = (CodeSystem) t.loadContainedResource(id, CodeSystem.class); + if (cs != null) { + localSystems.add(cs); + } + } + } else { + throw new Error("Not done yet #2: "+ref); + } + } + } public ValidationResult validateCode(CodeableConcept code) throws FHIRException { // first, we validate the codings themselves @@ -85,7 +128,7 @@ public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChe if (!c.hasSystem()) { warnings.add(context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE)); } - CodeSystem cs = context.fetchCodeSystem(c.getSystem()); + CodeSystem cs = resolveCodeSystem(c.getSystem()); ValidationResult res = null; if (cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) { res = context.validateCode(options.noClient(), c, null); @@ -124,6 +167,19 @@ public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChe } } + public CodeSystem resolveCodeSystem(String system) { + for (CodeSystem t : localSystems) { + if (t.getUrl().equals(system)) { + return t; + } + } + CodeSystem cs = context.fetchCodeSystem(system); + if (cs == null) { + cs = findSpecialCodeSystem(system); + } + return cs; + } + public ValidationResult validateCode(Coding code) throws FHIRException { String warningMessage = null; // first, we validate the concept itself @@ -144,10 +200,7 @@ public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChe } inExpansion = checkExpansion(code); inInclude = checkInclude(code); - CodeSystem cs = context.fetchCodeSystem(system); - if (cs == null) { - cs = findSpecialCodeSystem(system); - } + CodeSystem cs = resolveCodeSystem(system); if (cs == null) { warningMessage = "Unable to resolve system "+system; if (!inExpansion) { @@ -498,7 +551,7 @@ public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChe if (vsi.hasFilter()) { return null; } - CodeSystem cs = context.fetchCodeSystem(vsi.getSystem()); + CodeSystem cs = resolveCodeSystem(vsi.getSystem()); if (cs == null) { return null; } @@ -604,7 +657,7 @@ public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChe if (!system.equals(vsi.getSystem())) return false; // ok, we need the code system - CodeSystem cs = context.fetchCodeSystem(system); + CodeSystem cs = resolveCodeSystem(system); if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) { // make up a transient value set with ValueSet vs = new ValueSet(); @@ -709,7 +762,7 @@ public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChe return inner.get(url); } ValueSet vs = context.fetchResource(ValueSet.class, url); - ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, context); + ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, context, localContext); inner.put(url, vsc); return vsc; } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/validation/ValidationContextCarrier.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/validation/ValidationContextCarrier.java new file mode 100644 index 000000000..313919812 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/validation/ValidationContextCarrier.java @@ -0,0 +1,81 @@ +package org.hl7.fhir.r5.utils.validation; + +import java.util.ArrayList; +import java.util.List; + +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.elementmodel.Element; +import org.hl7.fhir.r5.model.DomainResource; +import org.hl7.fhir.r5.model.Questionnaire; +import org.hl7.fhir.r5.model.Resource; +import org.hl7.fhir.utilities.validation.ValidationMessage; + +public class ValidationContextCarrier { + /** + * + * When the validator is calling validateCode, it typically has a partially loaded resource that may provide + * additional resources that are relevant to the validation. This is a handle back into the validator context + * to ask for the resource to be fully loaded if it becomes relevant. Note that the resource may fail to load + * (e.g. if it's part of what's being validated) and if it does, the validator will record the validation + * issues before throwing an error + * + * This is a reference back int + * + */ + public interface IValidationContextResourceLoader { + public Resource loadContainedResource(List errors, String path, Element resource, String id, Class class1) throws FHIRException; + } + + /** + * A list of resources that provide context - typically, a container resource, and a bundle resource. + * iterate these in order looking for contained resources + * + */ + public static class ValidationContextResourceProxy { + + // either a resource + private Resource resource; + + + // or an element and a loader + private Element element; + private IValidationContextResourceLoader loader; + private List errors; + private String path; + + public ValidationContextResourceProxy(Resource resource) { + this.resource = resource; + } + + public ValidationContextResourceProxy(List errors, String path, Element element, IValidationContextResourceLoader loader) { + this.errors = errors; + this.path = path; + this.element = element; + this.loader = loader; + } + + public Resource loadContainedResource(String id, Class class1) throws FHIRException { + if (resource == null) { + Resource res = loader.loadContainedResource(errors, path, element, id, class1); + return res; + } else { + if (resource instanceof DomainResource) { + for (Resource r : ((DomainResource) resource).getContained()) { + if (r.getId().equals(id)) { + if (class1.isInstance(r)) + return r; + } + } + } + return null; + } + } + } + + private List resources = new ArrayList<>(); + + public List getResources() { + return resources; + } + +} 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 5eaf4c981..1a67ff30d 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 @@ -514,6 +514,7 @@ public class I18nConstants { public static final String TYPE_SPECIFIC_CHECKS_DT_URL_EXAMPLE = "TYPE_SPECIFIC_CHECKS_DT_URL_EXAMPLE"; public static final String TYPE_SPECIFIC_CHECKS_DT_CANONICAL_TYPE = "TYPE_SPECIFIC_CHECKS_DT_CANONICAL_TYPE"; public static final String TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE = "TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE"; + public static final String TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE_NC = "TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE_NC"; public static final String TYPE_SPECIFIC_CHECKS_DT_UUID_STRAT = "Type_Specific_Checks_DT_UUID_Strat"; public static final String TYPE_SPECIFIC_CHECKS_DT_UUID_VALID = "Type_Specific_Checks_DT_UUID_Valid"; public static final String UNABLE_TO_CONNECT_TO_TERMINOLOGY_SERVER = "Unable_to_connect_to_terminology_server"; diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 7c435856d..bf567fc41 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -645,6 +645,7 @@ SD_ED_BIND_NO_BINDABLE = The element {0} has a binding, but no bindable types ar DISCRIMINATOR_BAD_PATH = Error processing path expression for discriminator: {0} (src = ''{1}'') SLICING_CANNOT_BE_EVALUATED = Slicing cannot be evaluated: {0} TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE = Canonical URL ''{0}'' does not resolve +TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE_NC = Canonical URL ''{0}'' exists, but can't be loaded, so it can't be checked for validity TYPE_SPECIFIC_CHECKS_DT_CANONICAL_TYPE = Canonical URL ''{0}'' refers to a resource that has the wrong type. Found {1} expecting one of {2} CODESYSTEM_CS_NO_SUPPLEMENT = CodeSystem {0} is a supplement, so can't be used as a value in Coding.system CODESYSTEM_CS_SUPP_CANT_CHECK = CodeSystem {0} cannot be found, so can't check if concepts are valid diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java index fe10e899f..74c3102a1 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java @@ -44,6 +44,7 @@ import org.hl7.fhir.r5.model.*; import org.hl7.fhir.r5.terminologies.ValueSetUtilities; import org.hl7.fhir.r5.utils.XVerExtensionManager; import org.hl7.fhir.r5.utils.XVerExtensionManager.XVerExtensionStatus; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.IValidationContextResourceLoader; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.i18n.I18nConstants; import org.hl7.fhir.utilities.validation.ValidationMessage; @@ -61,7 +62,8 @@ import java.util.Map; import static org.apache.commons.lang3.StringUtils.isBlank; -public class BaseValidator { +public class BaseValidator implements IValidationContextResourceLoader { + public class TrackedLocationRelatedMessage { private Object location; private ValidationMessage vmsg; @@ -996,7 +998,8 @@ public class BaseValidator { } } - protected Resource loadContainedResource(List errors, String path, Element resource, String id, Class class1) throws FHIRException { + @Override + public Resource loadContainedResource(List errors, String path, Element resource, String id, Class class1) throws FHIRException { for (Element contained : resource.getChildren("contained")) { if (contained.getIdBase().equals(id)) { return loadFoundResource(errors, path, contained, class1); @@ -1005,7 +1008,7 @@ public class BaseValidator { return null; } - protected Resource loadFoundResource(List errors, String path, Element resource, Class class1) throws FHIRException { + protected Resource loadFoundResource(List errors, String path, Element resource, Class class1) throws FHIRException { try { FhirPublication v = FhirPublication.fromCode(context.getVersion()); ByteArrayOutputStream bs = new ByteArrayOutputStream(); diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/QuestionnaireValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/QuestionnaireValidator.java index 73c70bda2..947f3ec59 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/QuestionnaireValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/QuestionnaireValidator.java @@ -10,6 +10,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.attoparser.config.ParseConfiguration.ElementBalancing; import org.hl7.fhir.convertors.conv10_50.VersionConvertor_10_50; import org.hl7.fhir.convertors.conv14_50.VersionConvertor_14_50; import org.hl7.fhir.convertors.conv30_50.VersionConvertor_30_50; @@ -40,6 +41,8 @@ import org.hl7.fhir.r5.model.TimeType; import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.utils.FHIRPathEngine; import org.hl7.fhir.r5.utils.XVerExtensionManager; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy; import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.i18n.I18nConstants; @@ -52,6 +55,7 @@ import org.hl7.fhir.validation.cli.utils.QuestionnaireMode; import org.hl7.fhir.validation.TimeTracker; import org.hl7.fhir.validation.instance.EnableWhenEvaluator; import org.hl7.fhir.validation.instance.EnableWhenEvaluator.QStack; +import org.hl7.fhir.validation.instance.type.QuestionnaireValidator.ElementWithIndex; import org.hl7.fhir.validation.instance.type.QuestionnaireValidator.QuestionnaireWithContext; import org.hl7.fhir.validation.instance.utils.NodeStack; import org.hl7.fhir.validation.instance.utils.ValidatorHostContext; @@ -60,6 +64,26 @@ import ca.uhn.fhir.util.ObjectUtil; public class QuestionnaireValidator extends BaseValidator { + public class ElementWithIndex { + + private Element element; + private int index; + + public ElementWithIndex(Element element, int index) { + this.element = element; + this.index = index; + } + + public Element getElement() { + return element; + } + + public int getIndex() { + return index; + } + + } + public static class QuestionnaireWithContext { private Questionnaire q; private Element container; @@ -88,7 +112,7 @@ public class QuestionnaireValidator extends BaseValidator { public Questionnaire q() { return q; } - + } private EnableWhenEvaluator myEnableWhenEvaluator; @@ -263,8 +287,9 @@ public class QuestionnaireValidator extends BaseValidator { if (answers.size() > 1) rule(errors, IssueType.INVALID, answers.get(1).line(), answers.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), I18nConstants.QUESTIONNAIRE_QR_ITEM_ONLYONEA); + int i = 0; for (Element answer : answers) { - NodeStack ns = stack.push(answer, -1, null, null); + NodeStack ns = stack.push(answer, i, null, null); if (qItem.getType() != null) { switch (qItem.getType()) { case GROUP: @@ -337,12 +362,15 @@ public class QuestionnaireValidator extends BaseValidator { case NULL: // no validation break; + case QUESTION: + throw new Error("Shouldn't get here?"); } } if (qItem.getType() != QuestionnaireItemType.GROUP) { // if it's a group, we already have an error before getting here, so no need to hammer away on that validateQuestionannaireResponseItems(hostContext, qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot, qstack); } + i++; } if (qItem.getType() == null) { fail(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTYPE, qItem.getLinkId()); @@ -363,14 +391,13 @@ public class QuestionnaireValidator extends BaseValidator { return !answers.isEmpty() || !qItem.getRequired() || qItem.getType() == QuestionnaireItemType.GROUP; } - private void validateQuestionnaireResponseItem(ValidatorHostContext hostcontext, QuestionnaireWithContext qsrc, QuestionnaireItemComponent qItem, List errors, List elements, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) { - if (elements.size() > 1) - rule(errors, IssueType.INVALID, elements.get(1).line(), elements.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), I18nConstants.QUESTIONNAIRE_QR_ITEM_ONLYONEI, qItem.getLinkId()); - int i = 0; - for (Element element : elements) { - NodeStack ns = stack.push(element, i, null, null); - validateQuestionnaireResponseItem(hostcontext, qsrc, qItem, errors, element, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, element)); - i++; + private void validateQuestionnaireResponseItem(ValidatorHostContext hostcontext, QuestionnaireWithContext qsrc, QuestionnaireItemComponent qItem, List errors, List elements, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) { + if (elements.size() > 1) { + rule(errors, IssueType.INVALID, elements.get(1).getElement().line(), elements.get(1).getElement().col(), stack.getLiteralPath(), qItem.getRepeats(), I18nConstants.QUESTIONNAIRE_QR_ITEM_ONLYONEI, qItem.getLinkId()); + } + for (ElementWithIndex element : elements) { + NodeStack ns = stack.push(element.getElement(), element.getIndex(), null, null); + validateQuestionnaireResponseItem(hostcontext, qsrc, qItem, errors, element.getElement(), ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, element.getElement())); } } @@ -386,8 +413,9 @@ public class QuestionnaireValidator extends BaseValidator { List items = new ArrayList(); element.getNamedChildren("item", items); // now, sort into stacks - Map> map = new HashMap>(); + Map> map = new HashMap>(); int lastIndex = -1; + int counter = 0; for (Element item : items) { String linkId = item.getNamedChildValue("linkId"); if (rule(errors, IssueType.REQUIRED, item.line(), item.col(), stack.getLiteralPath(), !Utilities.noString(linkId), I18nConstants.QUESTIONNAIRE_QR_ITEM_NOLINKID)) { @@ -396,7 +424,7 @@ public class QuestionnaireValidator extends BaseValidator { QuestionnaireItemComponent qItem = findQuestionnaireItem(qsrc, linkId); if (qItem != null) { rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, misplacedItemError(qItem)); - NodeStack ns = stack.push(item, -1, null, null); + NodeStack ns = stack.push(item, counter, null, null); validateQuestionnaireResponseItem(hostContext, qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, item)); } else rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTFOUND, linkId); @@ -407,29 +435,28 @@ public class QuestionnaireValidator extends BaseValidator { // If an item has a child called "linkId" but no child called "answer", // we'll treat it as not existing for the purposes of enableWhen validation if (item.hasChildren("answer") || item.hasChildren("item")) { - List mapItem = map.computeIfAbsent(linkId, key -> new ArrayList<>()); - mapItem.add(item); + List mapItem = map.computeIfAbsent(linkId, key -> new ArrayList<>()); + mapItem.add(new ElementWithIndex(item, counter)); } } } + counter++; } // ok, now we have a list of known items, grouped by linkId. We've made an error for anything out of order for (QuestionnaireItemComponent qItem : qItems) { - List mapItem = map.get(qItem.getLinkId()); + List mapItem = map.get(qItem.getLinkId()); validateQuestionnaireResponseItem(hostContext, qsrc, errors, element, stack, inProgress, questionnaireResponseRoot, qItem, mapItem, qstack); } } - public void validateQuestionnaireResponseItem(ValidatorHostContext hostContext, QuestionnaireWithContext qsrc, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QuestionnaireItemComponent qItem, List mapItem, QStack qstack) { + public void validateQuestionnaireResponseItem(ValidatorHostContext hostContext, QuestionnaireWithContext qsrc, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QuestionnaireItemComponent qItem, List mapItem, QStack qstack) { boolean enabled = myEnableWhenEvaluator.isQuestionEnabled(hostContext, qItem, qstack, fpe); if (mapItem != null) { if (!enabled) { - int i = 0; - for (Element e : mapItem) { - NodeStack ns = stack.push(e, i, e.getProperty().getDefinition(), e.getProperty().getDefinition()); - rule(errors, IssueType.INVALID, e.line(), e.col(), ns.getLiteralPath(), enabled, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTENABLED2, qItem.getLinkId()); - i++; + for (ElementWithIndex e : mapItem) { + NodeStack ns = stack.push(e.getElement(), e.getElement().getIndex(), e.getElement().getProperty().getDefinition(), e.getElement().getProperty().getDefinition()); + rule(errors, IssueType.INVALID, e.getElement().line(), e.getElement().col(), ns.getLiteralPath(), enabled, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTENABLED2, qItem.getLinkId()); } } @@ -516,7 +543,8 @@ public class QuestionnaireValidator extends BaseValidator { } long t = System.nanoTime(); - ValidationResult res = context.validateCode(new ValidationOptions(stack.getWorkingLang()), c, vs); + ValidationContextCarrier vc = makeValidationContext(errors, qSrc); + ValidationResult res = context.validateCode(new ValidationOptions(stack.getWorkingLang()), c, vs, vc); timeTracker.tx(t, "vc "+c.getSystem()+"#"+c.getCode()+" '"+c.getDisplay()+"'"); if (!res.isOk()) { txRule(errors, res.getTxLink(), IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_BADOPTION, c.getSystem(), c.getCode()); @@ -529,6 +557,16 @@ public class QuestionnaireValidator extends BaseValidator { } } + private ValidationContextCarrier makeValidationContext(List errors, QuestionnaireWithContext qSrc) { + ValidationContextCarrier vc = new ValidationContextCarrier(); + if (qSrc.container == null) { + vc.getResources().add(new ValidationContextResourceProxy(qSrc.q)); + } else { + vc.getResources().add(new ValidationContextResourceProxy(errors, qSrc.containerPath, qSrc.container, this)); + } + return vc; + } + private void validateAnswerCode(List errors, Element answer, NodeStack stack, QuestionnaireWithContext qSrc, QuestionnaireItemComponent qItem, boolean theOpenChoice) { Element v = answer.getNamedChild("valueCoding"); NodeStack ns = stack.push(v, -1, null, null); From ac5e2b14a24dc6a0e1b908d6f2295272ff52d11b Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Mon, 13 Dec 2021 19:22:18 +1100 Subject: [PATCH 4/4] missing file --- .../src/main/java/org/hl7/fhir/r5/utils/ToolingExtensions.java | 1 + 1 file changed, 1 insertion(+) diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/ToolingExtensions.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/ToolingExtensions.java index 130b4e00a..ef912788e 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/ToolingExtensions.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/ToolingExtensions.java @@ -197,6 +197,7 @@ public class ToolingExtensions { public static final String EXT_BINARY_FORMAT = "http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format"; public static final String EXT_TARGET_ID = "http://hl7.org/fhir/StructureDefinition/targetElement"; public static final String EXT_TARGET_PATH = "http://hl7.org/fhir/StructureDefinition/targetPath"; + public static final String EXT_VALUESET_SYSTEM = "http://hl7.org/fhir/StructureDefinition/valueset-system"; // specific extension helpers