From 1c320586e57f545295a15324fd08d6bc143f3e53 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 22 Apr 2021 13:27:32 +1000 Subject: [PATCH] Ensure that CVX uses tx.fhir.org, not UTG definitions which are wrong + Fix problems with Bundle validation for ids in collections and add additional search related validation + Remove check on ElementDefinition.id for R2B --- RELEASE_NOTES.md | 3 + org.hl7.fhir.core.generator/readme.md | 2 +- .../terminologies/ValueSetCheckerSimple.java | 2 +- .../terminologies/ValueSetExpanderSimple.java | 4 +- .../fhir/r5/terminologies/ValueSetWorker.java | 10 ++ .../fhir/utilities/i18n/I18nConstants.java | 9 + .../src/main/resources/Messages.properties | 9 + .../hl7/fhir/validation/BaseValidator.java | 2 + .../instance/InstanceValidator.java | 20 ++- .../instance/type/BundleValidator.java | 156 ++++++++++++++++++ 10 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetWorker.java diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e69de29bb..e2c74a48a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -0,0 +1,3 @@ +Terminology: Ensure that CVX uses tx.fhir.org, not UTG definitions which are wrong +Validator: Fix problems with Bundle validation for ids in collections and add additional search related validation +Validator: Remove check on ElementDefinition.id for R2B diff --git a/org.hl7.fhir.core.generator/readme.md b/org.hl7.fhir.core.generator/readme.md index 3da59d227..ce13f4ee0 100644 --- a/org.hl7.fhir.core.generator/readme.md +++ b/org.hl7.fhir.core.generator/readme.md @@ -1,6 +1,6 @@ The Java Core Code Generator -Note: This code only generates tje R5 java code. Older generated models are now maintained by hand. +Note: This code only generates the R5 java code. Older generated models are now maintained by hand. To run this code, run the class JavaCoreGenerator with 3 parameters: * 1: fhir version to generate from (e.g. 4.1.0 or 'current' 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 fa446b133..d96857ae0 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 @@ -63,7 +63,7 @@ import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; import org.hl7.fhir.utilities.validation.ValidationOptions; import org.hl7.fhir.utilities.validation.ValidationOptions.ValueSetMode; -public class ValueSetCheckerSimple implements ValueSetChecker { +public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChecker { private ValueSet valueset; private IWorkerContext context; diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetExpanderSimple.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetExpanderSimple.java index 86f0477e3..382c10419 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetExpanderSimple.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetExpanderSimple.java @@ -108,7 +108,7 @@ import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionParameterComponent; import org.hl7.fhir.r5.utils.ToolingExtensions; import org.hl7.fhir.utilities.Utilities; -public class ValueSetExpanderSimple implements ValueSetExpander { +public class ValueSetExpanderSimple extends ValueSetWorker implements ValueSetExpander { public class PropertyFilter implements IConceptFilter { @@ -567,7 +567,7 @@ public class ValueSetExpanderSimple implements ValueSetExpander { copyImportContains(base.getExpansion().getContains(), null, expParams, imports); } else { CodeSystem cs = context.fetchCodeSystem(inc.getSystem()); - if ((cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT))) { + if (isServerSide(inc.getSystem()) || (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT))) { doServerIncludeCodes(inc, heirarchical, exp, imports, expParams, extensions); } else { doInternalIncludeCodes(inc, exp, expParams, imports, cs); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetWorker.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetWorker.java new file mode 100644 index 000000000..377227e9e --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetWorker.java @@ -0,0 +1,10 @@ +package org.hl7.fhir.r5.terminologies; + +import org.hl7.fhir.utilities.Utilities; + +public class ValueSetWorker { + + protected boolean isServerSide(String url) { + return Utilities.existsInList(url, "http://hl7.org/fhir/sid/cvx"); + } +} 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 96ac55b96..5701932ac 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 @@ -631,6 +631,15 @@ public class I18nConstants { public static final String BUNDLE_RULE_UNKNOWN = "BUNDLE_RULE_UNKNOWN"; public static final String BUNDLE_RULE_INVALID_INDEX = "BUNDLE_RULE_INVALID_INDEX"; public static final String BUNDLE_RULE_PROFILE_UNKNOWN = "BUNDLE_RULE_PROFILE_UNKNOWN"; + public static final String BUNDLE_SEARCH_NOSELF = "BUNDLE_SEARCH_NOSELF"; + public static final String BUNDLE_SEARCH_SELF_NOT_UNDERSTOOD = "BUNDLE_SEARCH_SELF_NOT_UNDERSTOOD"; + public static final String BUNDLE_SEARCH_ENTRY_NO_RESOURCE = "BUNDLE_SEARCH_ENTRY_NO_RESOURCE"; + public static final String BUNDLE_SEARCH_ENTRY_TYPE_NOT_SURE = "BUNDLE_SEARCH_ENTRY_TYPE_NOT_SURE"; + public static final String BUNDLE_SEARCH_ENTRY_NO_RESOURCE_ID = "BUNDLE_SEARCH_ENTRY_NO_RESOURCE_ID"; + public static final String BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_MODE = "BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_MODE"; + public static final String BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_NO_MODE = "BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_NO_MODE"; + public static final String BUNDLE_SEARCH_NO_MODE = "BUNDLE_SEARCH_NO_MODE"; + public static final String BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_OUTCOME = "BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_OUTCOME"; } diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 18a850034..24971caef 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -643,3 +643,12 @@ CODESYSTEM_CS_SUPP_INVALID_CODE = The code ''{1}'' is not declared in the base C SD_VALUE_TYPE_IILEGAL = The element {0} has a {1} of type {2}, which is not in the list of allowed types ({3}) SD_NO_TYPES_OR_CONTENTREF = The element {0} has no assigned types, and no content reference CODESYSTEM_CS_UNK_EXPANSION = The code provided ({2}) is not in the value set {0}, and a code is required from this value set. The system {1} is unknown. +BUNDLE_SEARCH_NOSELF = SearchSet Bundles should have a self link that specifies what the search was +BUNDLE_SEARCH_SELF_NOT_UNDERSTOOD = No types could be determined from the search string, so the types can't be checked +BUNDLE_SEARCH_ENTRY_NO_RESOURCE = SearchSet Bundle Entries must have resources +BUNDLE_SEARCH_ENTRY_TYPE_NOT_SURE = Unable to determine if this resource is a valid resource type for this search +BUNDLE_SEARCH_ENTRY_NO_RESOURCE_ID = Search results must have ids +BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_MODE = This is not a matching resource type for the specified search ({0} expecting {1}) +BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_OUTCOME = This is not an OperationOutcome ({0}) +BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_NO_MODE = This is not a matching resource type for the specified search (is a search mode needed?) ({0} expecting {1}) +BUNDLE_SEARCH_NO_MODE = SearchSet bundles should have search modes on the entries 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 0b0ac7f16..e845dc6dc 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 @@ -125,9 +125,11 @@ public class BaseValidator { protected final String META = "meta"; protected final String ENTRY = "entry"; + protected final String LINK = "link"; protected final String DOCUMENT = "document"; protected final String RESOURCE = "resource"; protected final String MESSAGE = "message"; + protected final String SEARCHSET = "searchset"; protected final String ID = "id"; protected final String FULL_URL = "fullUrl"; protected final String PATH_ARG = ":0"; 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 a1a8742dc..7fb09409f 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 @@ -2005,7 +2005,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } if (type.equals(ID)) { // work around an old issue with ElementDefinition.id - if (!context.getPath().equals("ElementDefinition.id") || VersionUtilities.versionsCompatible("1.4", this.context.getVersion())) { + if (!context.getPath().equals("ElementDefinition.id")) { rule(errors, IssueType.INVALID, e.line(), e.col(), path, FormatUtilities.isValidId(e.primitiveValue()), I18nConstants.TYPE_SPECIFIC_CHECKS_DT_ID_VALID, e.primitiveValue()); } } @@ -4166,6 +4166,24 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat hc = hostContext.forContained(element); } stack.resetIds(); + if (element.getSpecial() != null) { + switch (element.getSpecial()) { + case BUNDLE_ENTRY: + idstatus = IdStatus.OPTIONAL; + break; + case BUNDLE_OUTCOME: + idstatus = IdStatus.OPTIONAL; + break; + case CONTAINED: + idstatus = IdStatus.REQUIRED; + break; + case PARAMETER: + idstatus = IdStatus.OPTIONAL; + break; + default: + break; + } + } if (trr.getProfile().size() == 1) { long t = System.nanoTime(); StructureDefinition profile = this.context.fetchResource(StructureDefinition.class, trr.getProfile().get(0).asStringValue()); diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/BundleValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/BundleValidator.java index 187c3a632..f321ac948 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/BundleValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/BundleValidator.java @@ -73,6 +73,9 @@ public class BundleValidator extends BaseValidator{ } checkAllInterlinked(errors, entries, stack, bundle, VersionUtilities.isR5Ver(context.getVersion())); } + if (type.equals(SEARCHSET)) { + checkSearchSet(errors, bundle, entries, stack); + } // We do not yet have rules requiring that the id and fullUrl match when dealing with messaging Bundles // validateResourceIds(errors, entries, stack); } @@ -122,6 +125,159 @@ public class BundleValidator extends BaseValidator{ } } + private void checkSearchSet(List errors, Element bundle, List entries, NodeStack stack) { + // warning: should have self link + List links = new ArrayList(); + bundle.getNamedChildren(LINK, links); + Element selfLink = getSelfLink(links); + List types = new ArrayList<>(); + if (selfLink == null) { + warning(errors, IssueType.INVALID, bundle.line(), bundle.col(), stack.getLiteralPath(), false, I18nConstants.BUNDLE_SEARCH_NOSELF); + } else { + readSearchResourceTypes(selfLink.getNamedChildValue("url"), types); + if (types.size() == 0) { + hint(errors, IssueType.INVALID, bundle.line(), bundle.col(), stack.getLiteralPath(), false, I18nConstants.BUNDLE_SEARCH_SELF_NOT_UNDERSTOOD); + } + } + + Boolean searchMode = readHasSearchMode(entries); + if (searchMode == false) { // if no resources have search mode + boolean typeProblem = false; + String rtype = null; + int count = 0; + for (Element entry : entries) { + NodeStack estack = stack.push(entry, count, null, null); + count++; + Element res = entry.getNamedChild("resource"); + if (rule(errors, IssueType.INVALID, bundle.line(), bundle.col(), estack.getLiteralPath(), res != null, I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE)) { + NodeStack rstack = estack.push(res, -1, null, null); + String rt = res.fhirType(); + Boolean ok = checkSearchType(types, rt); + if (ok == null) { + typeProblem = true; + hint(errors, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), selfLink == null, I18nConstants.BUNDLE_SEARCH_ENTRY_TYPE_NOT_SURE); + String id = res.getNamedChildValue("id"); + warning(errors, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), id != null || "OperationOutcome".equals(rt), I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE_ID); + } else if (ok) { + if (!"OperationOutcome".equals(rt)) { + String id = res.getNamedChildValue("id"); + warning(errors, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), id != null, I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE_ID); + if (rtype != null && !rt.equals(rtype)) { + typeProblem = true; + } else if (rtype == null) { + rtype = rt; + } + } + } else { + typeProblem = true; + warning(errors, IssueType.INVALID, bundle.line(), bundle.col(), estack.getLiteralPath(), false, I18nConstants.BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_NO_MODE, rt, types); + } + } + } + if (typeProblem) { + warning(errors, IssueType.INVALID, bundle.line(), bundle.col(), stack.getLiteralPath(), !typeProblem, I18nConstants.BUNDLE_SEARCH_NO_MODE); + } else { + hint(errors, IssueType.INVALID, bundle.line(), bundle.col(), stack.getLiteralPath(), !typeProblem, I18nConstants.BUNDLE_SEARCH_NO_MODE); + } + } else { + int count = 0; + for (Element entry : entries) { + NodeStack estack = stack.push(entry, count, null, null); + count++; + Element res = entry.getNamedChild("resource"); + String sm = null; + Element s = entry.getNamedChild("search"); + if (s != null) { + sm = s.getNamedChildValue("mode"); + } + warning(errors, IssueType.INVALID, bundle.line(), bundle.col(), estack.getLiteralPath(), sm != null, I18nConstants.BUNDLE_SEARCH_NO_MODE); + if (rule(errors, IssueType.INVALID, bundle.line(), bundle.col(), estack.getLiteralPath(), res != null, I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE)) { + NodeStack rstack = estack.push(res, -1, null, null); + String rt = res.fhirType(); + String id = res.getNamedChildValue("id"); + if (sm != null) { + if ("match".equals(sm)) { + rule(errors, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), id != null, I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE_ID); + rule(errors, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), types.size() == 0 || checkSearchType(types, rt), I18nConstants.BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_MODE, rt, types); + } else if ("include".equals(sm)) { + rule(errors, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), id != null, I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE_ID); + } else { // outcome + rule(errors, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), "OperationOutcome".equals(rt), I18nConstants.BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_OUTCOME, rt); + } + } + } + } + } + } + + private Boolean checkSearchType(List types, String rt) { + if (types.size() == 0) { + return null; + } else { + return Utilities.existsInList(rt, types); + } + } + + private Boolean readHasSearchMode(List entries) { + boolean all = true; + boolean any = false; + for (Element entry : entries) { + String sm = null; + Element s = entry.getNamedChild("search"); + if (s != null) { + sm = s.getNamedChildValue("mode"); + } + if (sm != null) { + any = true; + } else { + all = false; + } + } + if (all) { + return true; + } else if (any) { + return null; + } else { + return false; + } + } + + private void readSearchResourceTypes(String ref, List types) { + if (ref == null) { + return; + } + String[] head = null; + String[] tail = null; + if (ref.contains("?")) { + head = ref.substring(0, ref.indexOf("?")).split("\\/"); + tail = ref.substring(ref.indexOf("?")+1).split("\\&"); + } else { + head = ref.split("\\/"); + } + if (head == null || head.length == 0) { + return; + } else if (context.getResourceNames().contains(head[head.length-1])) { + types.add(head[head.length-1]); + } else if (tail != null) { + for (String s : tail) { + if (s.startsWith("_type=")) { + for (String t : s.substring(6).split("\\,")) { + types.add(t); + } + } + } + } + } + + private Element getSelfLink(List links) { + for (Element link : links) { + if ("self".equals(link.getNamedChildValue("relation"))) { + return link; + } + } + return null; + } + private void validateDocument(List errors, List entries, Element composition, NodeStack stack, String fullUrl, String id) { // first entry must be a composition if (rule(errors, IssueType.INVALID, composition.line(), composition.col(), stack.getLiteralPath(), composition.getType().equals("Composition"), I18nConstants.BUNDLE_BUNDLE_ENTRY_DOCUMENT)) {