From d688f9254d801b2ad871bcad72373661f0fc39d6 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Wed, 13 May 2020 09:45:29 +1000 Subject: [PATCH] * fix fatal NPE validating bundles when resource is missing * fix tests for R5 changes --- .../utilities/cache/PackageCacheManager.java | 12 +- .../utilities/tests/PackageCacheTests.java | 4 +- .../hl7/fhir/validation/BaseValidator.java | 182 ++++++ .../instance/InstanceValidator.java | 524 +----------------- .../instance/type/BundleValidator.java | 375 +++++++++++++ release-notes-validator.md | 4 +- 6 files changed, 579 insertions(+), 522 deletions(-) create mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/BundleValidator.java diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/cache/PackageCacheManager.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/cache/PackageCacheManager.java index 2a8c177c8..85d2bb0bf 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/cache/PackageCacheManager.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/cache/PackageCacheManager.java @@ -329,8 +329,6 @@ public class PackageCacheManager { } private InputStreamWithSrc loadFromPackageServer(String id, String v) { - // release hack: - if ("4.4.0".equals(v)) {v = "4.2.0"; }; PackageClient pc = new PackageClient(PRIMARY_SERVER); String u = null; InputStream stream; @@ -861,6 +859,16 @@ public class PackageCacheManager { return null; } + public List listPackages() { + List res = new ArrayList<>(); + for (File f : new File(cacheFolder).listFiles()) { + if (f.isDirectory() && f.getName().contains("#")) { + res.add(f.getName()); + } + } + return res; + } + //public List getUrls() throws IOException { diff --git a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/tests/PackageCacheTests.java b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/tests/PackageCacheTests.java index 5baedc24e..46735d07f 100644 --- a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/tests/PackageCacheTests.java +++ b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/tests/PackageCacheTests.java @@ -14,11 +14,10 @@ import java.io.IOException; public class PackageCacheTests { @Test - @Disabled // This test is currently set to always fail. public void testPath() throws IOException { PackageCacheManager cache = new PackageCacheManager(true, ToolsVersion.TOOLS_VERSION); cache.clear(); - Assertions.assertTrue(false); + Assertions.assertTrue(cache.listPackages().isEmpty()); NpmPackage npm = cache.loadPackage("hl7.fhir.pubpack", "0.0.3"); npm.loadAllFiles(); Assertions.assertNotNull(npm); @@ -31,5 +30,6 @@ public class PackageCacheTests { npm.save(dir); NpmPackage npm2 = cache.loadPackage("hl7.fhir.pubpack", "file:" + dir.getAbsolutePath()); Assertions.assertNotNull(npm2); + Assertions.assertFalse(cache.listPackages().isEmpty()); } } \ No newline at end of file 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 50e41b651..37ad02c19 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 @@ -2,6 +2,8 @@ package org.hl7.fhir.validation; import static org.apache.commons.lang3.StringUtils.isBlank; +import java.util.ArrayList; + /* Copyright (c) 2011+, HL7, Inc. All rights reserved. @@ -64,18 +66,35 @@ POSSIBILITY OF SUCH DAMAGE. import java.util.List; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.elementmodel.Element; +import org.hl7.fhir.r5.model.Base; import org.hl7.fhir.r5.model.DomainResource; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.terminologies.ValueSetUtilities; import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.i18n.I18nConstants; import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; import org.hl7.fhir.utilities.validation.ValidationMessage.Source; +import org.hl7.fhir.validation.instance.utils.IndexedElement; public class BaseValidator { + protected final String META = "meta"; + protected final String ENTRY = "entry"; + protected final String DOCUMENT = "document"; + protected final String RESOURCE = "resource"; + protected final String MESSAGE = "message"; + protected final String ID = "id"; + protected final String FULL_URL = "fullUrl"; + protected final String PATH_ARG = ":0"; + protected final String TYPE = "type"; + protected final String BUNDLE = "Bundle"; + protected final String LAST_UPDATED = "lastUpdated"; + + protected Source source; protected IWorkerContext context; protected TimeTracker timeTracker = new TimeTracker(); @@ -616,5 +635,168 @@ public class BaseValidator { return reference; } + protected Base resolveInBundle(String url, Element bnd) { + if (bnd == null) + return null; + if (bnd.fhirType().equals(BUNDLE)) { + for (Element be : bnd.getChildrenByName(ENTRY)) { + Element res = be.getNamedChild(RESOURCE); + if (res != null) { + String fullUrl = be.getChildValue(FULL_URL); + String rt = res.fhirType(); + String id = res.getChildValue(ID); + if (url.equals(fullUrl)) + return res; + if (url.equals(rt + "/" + id)) + return res; + } + } + } + return null; + } + + protected Element resolveInBundle(List entries, String ref, String fullUrl, String type, String id) { + if (Utilities.isAbsoluteUrl(ref)) { + // if the reference is absolute, then you resolve by fullUrl. No other thinking is required. + for (Element entry : entries) { + String fu = entry.getNamedChildValue(FULL_URL); + if (ref.equals(fu)) + return entry; + } + return null; + } else { + // split into base, type, and id + String u = null; + if (fullUrl != null && fullUrl.endsWith(type + "/" + id)) + // fullUrl = complex + u = fullUrl.substring(0, fullUrl.length() - (type + "/" + id).length()) + ref; +// u = fullUrl.substring((type+"/"+id).length())+ref; + String[] parts = ref.split("\\/"); + if (parts.length >= 2) { + String t = parts[0]; + String i = parts[1]; + for (Element entry : entries) { + String fu = entry.getNamedChildValue(FULL_URL); + if (fu != null && fu.equals(u)) + return entry; + if (u == null) { + Element resource = entry.getNamedChild(RESOURCE); + if (resource != null) { + String et = resource.getType(); + String eid = resource.getNamedChildValue(ID); + if (t.equals(et) && i.equals(eid)) + return entry; + } + } + } + } + return null; + } + } + + + protected IndexedElement getFromBundle(Element bundle, String ref, String fullUrl, List errors, String path, String type, boolean isTransaction) { + String targetUrl = null; + String version = ""; + String resourceType = null; + if (ref.startsWith("http") || ref.startsWith("urn")) { + // We've got an absolute reference, no need to calculate + if (ref.contains("/_history/")) { + targetUrl = ref.substring(0, ref.indexOf("/_history/") - 1); + version = ref.substring(ref.indexOf("/_history/") + 10); + } else + targetUrl = ref; + + } else if (fullUrl == null) { + //This isn't a problem for signatures - if it's a signature, we won't have a resolution for a relative reference. For anything else, this is an error + // but this rule doesn't apply for batches or transactions + rule(errors, IssueType.REQUIRED, -1, -1, path, Utilities.existsInList(type, "batch-response", "transaction-response") || path.startsWith("Bundle.signature"), I18nConstants.BUNDLE_BUNDLE_FULLURL_MISSING); + return null; + + } else if (ref.split("/").length != 2 && ref.split("/").length != 4) { + if (isTransaction) { + rule(errors, IssueType.INVALID, -1, -1, path, isSearchUrl(ref), I18nConstants.REFERENCE_REF_FORMAT1, ref); + } else { + rule(errors, IssueType.INVALID, -1, -1, path, false, I18nConstants.REFERENCE_REF_FORMAT2, ref); + } + return null; + + } else { + String base = ""; + if (fullUrl.startsWith("urn")) { + String[] parts = fullUrl.split("\\:"); + for (int i = 0; i < parts.length - 1; i++) { + base = base + parts[i] + ":"; + } + } else { + String[] parts; + parts = fullUrl.split("/"); + for (int i = 0; i < parts.length - 2; i++) { + base = base + parts[i] + "/"; + } + } + + String id = null; + if (ref.contains("/_history/")) { + version = ref.substring(ref.indexOf("/_history/") + 10); + String[] refBaseParts = ref.substring(0, ref.indexOf("/_history/")).split("/"); + resourceType = refBaseParts[0]; + id = refBaseParts[1]; + } else if (base.startsWith("urn")) { + resourceType = ref.split("/")[0]; + id = ref.split("/")[1]; + } else + id = ref; + + targetUrl = base + id; + } + + List entries = new ArrayList(); + bundle.getNamedChildren(ENTRY, entries); + Element match = null; + int matchIndex = -1; + for (int i = 0; i < entries.size(); i++) { + Element we = entries.get(i); + if (targetUrl.equals(we.getChildValue(FULL_URL))) { + Element r = we.getNamedChild(RESOURCE); + if (version.isEmpty()) { + rule(errors, IssueType.FORBIDDEN, -1, -1, path, match == null, I18nConstants.BUNDLE_BUNDLE_MULTIPLEMATCHES, ref); + match = r; + matchIndex = i; + } else { + try { + if (version.equals(r.getChildren(META).get(0).getChildValue("versionId"))) { + rule(errors, IssueType.FORBIDDEN, -1, -1, path, match == null, I18nConstants.BUNDLE_BUNDLE_MULTIPLEMATCHES, ref); + match = r; + matchIndex = i; + } + } catch (Exception e) { + warning(errors, IssueType.REQUIRED, -1, -1, path, r.getChildren(META).size() == 1 && r.getChildren(META).get(0).getChildValue("versionId") != null, I18nConstants.BUNDLE_BUNDLE_FULLURL_NEEDVERSION, targetUrl); + // If one of these things is null + } + } + } + } + + if (match != null && resourceType != null) + rule(errors, IssueType.REQUIRED, -1, -1, path, match.getType().equals(resourceType), I18nConstants.REFERENCE_REF_RESOURCETYPE, ref, match.getType()); + if (match == null) + warning(errors, IssueType.REQUIRED, -1, -1, path, !ref.startsWith("urn"), I18nConstants.BUNDLE_BUNDLE_NOT_LOCAL, ref); + return match == null ? null : new IndexedElement(matchIndex, match, entries.get(matchIndex)); + } + + private boolean isSearchUrl(String ref) { + if (Utilities.noString(ref) || !ref.contains("?")) { + return false; + } + String tn = ref.substring(0, ref.indexOf("?")); + String q = ref.substring(ref.indexOf("?") + 1); + if (!context.getResourceNames().contains(tn)) { + return false; + } else { + return q.matches("([_a-zA-Z][_a-zA-Z0-9]*=[^=&]+)(&([_a-zA-Z][_a-zA-Z0-9]*=[^=&]+))*"); + } + } + } \ No newline at end of file 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 7ee425da1..d08a60051 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 @@ -149,6 +149,7 @@ import org.hl7.fhir.utilities.i18n.I18nConstants; import org.hl7.fhir.validation.BaseValidator; import org.hl7.fhir.validation.TimeTracker; import org.hl7.fhir.validation.instance.EnableWhenEvaluator.QStack; +import org.hl7.fhir.validation.instance.type.BundleValidator; import org.hl7.fhir.validation.instance.type.CodeSystemValidator; import org.hl7.fhir.validation.instance.type.MeasureValidator; import org.hl7.fhir.validation.instance.type.QuestionnaireValidator; @@ -188,19 +189,9 @@ import ca.uhn.fhir.util.ObjectUtil; */ public class InstanceValidator extends BaseValidator implements IResourceValidator { - - private final String META = "meta"; - private final String ENTRY = "entry"; - private final String DOCUMENT = "document"; - private final String RESOURCE = "resource"; - private final String MESSAGE = "message"; - private final String ID = "id"; - private final String PATH_ARG = ":0"; - private final String FULL_URL = "fullUrl"; - private final String TYPE = "type"; - private final String BUNDLE = "Bundle"; - private final String LAST_UPDATED = "lastUpdated"; - + private static final String EXECUTED_CONSTRAINT_LIST = "validator.executed.invariant.list"; + private static final String EXECUTION_ID = "validator.execution.id"; + private class ValidatorHostServices implements IEvaluationContext { @Override @@ -278,26 +269,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } - public Base resolveInBundle(String url, Element bnd) { - if (bnd == null) - return null; - if (bnd.fhirType().equals(BUNDLE)) { - for (Element be : bnd.getChildrenByName(ENTRY)) { - Element res = be.getNamedChild(RESOURCE); - if (res != null) { - String fullUrl = be.getChildValue(FULL_URL); - String rt = res.fhirType(); - String id = res.getChildValue(ID); - if (url.equals(fullUrl)) - return res; - if (url.equals(rt + "/" + id)) - return res; - } - } - } - return null; - } - + @Override public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException { ValidatorHostContext ctxt = (ValidatorHostContext) appContext; @@ -2595,109 +2567,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return extensionDomains; } - private IndexedElement getFromBundle(Element bundle, String ref, String fullUrl, List errors, String path, String type, boolean isTransaction) { - String targetUrl = null; - String version = ""; - String resourceType = null; - if (ref.startsWith("http") || ref.startsWith("urn")) { - // We've got an absolute reference, no need to calculate - if (ref.contains("/_history/")) { - targetUrl = ref.substring(0, ref.indexOf("/_history/") - 1); - version = ref.substring(ref.indexOf("/_history/") + 10); - } else - targetUrl = ref; - - } else if (fullUrl == null) { - //This isn't a problem for signatures - if it's a signature, we won't have a resolution for a relative reference. For anything else, this is an error - // but this rule doesn't apply for batches or transactions - rule(errors, IssueType.REQUIRED, -1, -1, path, Utilities.existsInList(type, "batch-response", "transaction-response") || path.startsWith("Bundle.signature"), I18nConstants.BUNDLE_BUNDLE_FULLURL_MISSING); - return null; - - } else if (ref.split("/").length != 2 && ref.split("/").length != 4) { - if (isTransaction) { - rule(errors, IssueType.INVALID, -1, -1, path, isSearchUrl(ref), I18nConstants.REFERENCE_REF_FORMAT1, ref); - } else { - rule(errors, IssueType.INVALID, -1, -1, path, false, I18nConstants.REFERENCE_REF_FORMAT2, ref); - } - return null; - - } else { - String base = ""; - if (fullUrl.startsWith("urn")) { - String[] parts = fullUrl.split("\\:"); - for (int i = 0; i < parts.length - 1; i++) { - base = base + parts[i] + ":"; - } - } else { - String[] parts; - parts = fullUrl.split("/"); - for (int i = 0; i < parts.length - 2; i++) { - base = base + parts[i] + "/"; - } - } - - String id = null; - if (ref.contains("/_history/")) { - version = ref.substring(ref.indexOf("/_history/") + 10); - String[] refBaseParts = ref.substring(0, ref.indexOf("/_history/")).split("/"); - resourceType = refBaseParts[0]; - id = refBaseParts[1]; - } else if (base.startsWith("urn")) { - resourceType = ref.split("/")[0]; - id = ref.split("/")[1]; - } else - id = ref; - - targetUrl = base + id; - } - - List entries = new ArrayList(); - bundle.getNamedChildren(ENTRY, entries); - Element match = null; - int matchIndex = -1; - for (int i = 0; i < entries.size(); i++) { - Element we = entries.get(i); - if (targetUrl.equals(we.getChildValue(FULL_URL))) { - Element r = we.getNamedChild(RESOURCE); - if (version.isEmpty()) { - rule(errors, IssueType.FORBIDDEN, -1, -1, path, match == null, I18nConstants.BUNDLE_BUNDLE_MULTIPLEMATCHES, ref); - match = r; - matchIndex = i; - } else { - try { - if (version.equals(r.getChildren(META).get(0).getChildValue("versionId"))) { - rule(errors, IssueType.FORBIDDEN, -1, -1, path, match == null, I18nConstants.BUNDLE_BUNDLE_MULTIPLEMATCHES, ref); - match = r; - matchIndex = i; - } - } catch (Exception e) { - warning(errors, IssueType.REQUIRED, -1, -1, path, r.getChildren(META).size() == 1 && r.getChildren(META).get(0).getChildValue("versionId") != null, I18nConstants.BUNDLE_BUNDLE_FULLURL_NEEDVERSION, targetUrl); - // If one of these things is null - } - } - } - } - - if (match != null && resourceType != null) - rule(errors, IssueType.REQUIRED, -1, -1, path, match.getType().equals(resourceType), I18nConstants.REFERENCE_REF_RESOURCETYPE, ref, match.getType()); - if (match == null) - warning(errors, IssueType.REQUIRED, -1, -1, path, !ref.startsWith("urn"), I18nConstants.BUNDLE_BUNDLE_NOT_LOCAL, ref); - return match == null ? null : new IndexedElement(matchIndex, match, entries.get(matchIndex)); - } - - private boolean isSearchUrl(String ref) { - if (Utilities.noString(ref) || !ref.contains("?")) { - return false; - } - String tn = ref.substring(0, ref.indexOf("?")); - String q = ref.substring(ref.indexOf("?") + 1); - if (!context.getResourceNames().contains(tn)) { - return false; - } else { - return q.matches("([_a-zA-Z][_a-zA-Z0-9]*=[^=&]+)(&([_a-zA-Z][_a-zA-Z0-9]*=[^=&]+))*"); - } - } - private StructureDefinition getProfileForType(String type, List list) { for (TypeRefComponent tr : list) { String url = tr.getWorkingCode(); @@ -2964,45 +2833,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } - private Element resolveInBundle(List entries, String ref, String fullUrl, String type, String id) { - if (Utilities.isAbsoluteUrl(ref)) { - // if the reference is absolute, then you resolve by fullUrl. No other thinking is required. - for (Element entry : entries) { - String fu = entry.getNamedChildValue(FULL_URL); - if (ref.equals(fu)) - return entry; - } - return null; - } else { - // split into base, type, and id - String u = null; - if (fullUrl != null && fullUrl.endsWith(type + "/" + id)) - // fullUrl = complex - u = fullUrl.substring(0, fullUrl.length() - (type + "/" + id).length()) + ref; -// u = fullUrl.substring((type+"/"+id).length())+ref; - String[] parts = ref.split("\\/"); - if (parts.length >= 2) { - String t = parts[0]; - String i = parts[1]; - for (Element entry : entries) { - String fu = entry.getNamedChildValue(FULL_URL); - if (fu != null && fu.equals(u)) - return entry; - if (u == null) { - Element resource = entry.getNamedChild(RESOURCE); - if (resource != null) { - String et = resource.getType(); - String eid = resource.getNamedChildValue(ID); - if (t.equals(et) && i.equals(eid)) - return entry; - } - } - } - } - return null; - } - } - private ElementDefinition resolveNameReference(StructureDefinitionSnapshotComponent snapshot, String contentReference) { for (ElementDefinition ed : snapshot.getElement()) if (contentReference.equals("#" + ed.getId())) @@ -3486,7 +3316,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat public void checkSpecials(ValidatorHostContext hostContext, List errors, Element element, NodeStack stack, boolean checkSpecials) { // specific known special validations if (element.getType().equals(BUNDLE)) { - validateBundle(errors, element, stack, checkSpecials); + new BundleValidator(context, serverBase).validateBundle(errors, element, stack, checkSpecials); } else if (element.getType().equals("Observation")) { validateObservation(errors, element, stack); } else if (element.getType().equals("Questionnaire")) { @@ -3583,268 +3413,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat iRest++; } } - - private void validateBundle(List errors, Element bundle, NodeStack stack, boolean checkSpecials) { - List entries = new ArrayList(); - bundle.getNamedChildren(ENTRY, entries); - String type = bundle.getNamedChildValue(TYPE); - type = StringUtils.defaultString(type); - - if (entries.size() == 0) { - rule(errors, IssueType.INVALID, stack.getLiteralPath(), !(type.equals(DOCUMENT) || type.equals(MESSAGE)), I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRST); - } else { - // Get the first entry, the MessageHeader - Element firstEntry = entries.get(0); - // Get the stack of the first entry - NodeStack firstStack = stack.push(firstEntry, 1, null, null); - - String fullUrl = firstEntry.getNamedChildValue(FULL_URL); - - if (type.equals(DOCUMENT)) { - Element resource = firstEntry.getNamedChild(RESOURCE); - String id = resource.getNamedChildValue(ID); - if (rule(errors, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), resource != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRSTRESOURCE)) { - validateDocument(errors, entries, resource, firstStack.push(resource, -1, null, null), fullUrl, id); - } - if (!VersionUtilities.isThisOrLater(FHIRVersion._4_0_1.getDisplay(), bundle.getProperty().getStructure().getFhirVersion().getDisplay())) { - handleSpecialCaseForLastUpdated(bundle, errors, stack); - } - checkAllInterlinked(errors, entries, stack, bundle, true); - } - if (type.equals(MESSAGE)) { - Element resource = firstEntry.getNamedChild(RESOURCE); - String id = resource.getNamedChildValue(ID); - if (rule(errors, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), resource != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRSTRESOURCE)) { - validateMessage(errors, entries, resource, firstStack.push(resource, -1, null, null), fullUrl, id); - } - checkAllInterlinked(errors, entries, stack, bundle, VersionUtilities.isR5Ver(context.getVersion())); - } - // We do not yet have rules requiring that the id and fullUrl match when dealing with messaging Bundles - // validateResourceIds(errors, entries, stack); - } - for (Element entry : entries) { - String fullUrl = entry.getNamedChildValue(FULL_URL); - String url = getCanonicalURLForEntry(entry); - String id = getIdForEntry(entry); - if (url != null) { - if (!(!url.equals(fullUrl) || (url.matches(uriRegexForVersion()) && url.endsWith("/" + id))) && !isV3orV2Url(url)) - rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), false, I18nConstants.BUNDLE_BUNDLE_ENTRY_MISMATCHIDURL, url, fullUrl, id); - rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), !url.equals(fullUrl) || serverBase == null || (url.equals(Utilities.pathURL(serverBase, entry.getNamedChild(RESOURCE).fhirType(), id))), I18nConstants.BUNDLE_BUNDLE_ENTRY_CANONICAL, url, fullUrl); - } - // todo: check specials - } - } - - /** - * As per outline for Document Content: - *
  • "The document date (mandatory). This is found in Bundle.meta.lastUpdated and identifies when the document bundle - * was assembled from the underlying resources"
  • - *

    - * This check was not being done for release versions < r4. - *

    - * Related JIRA ticket is FHIR-26544 - * - * @param bundle {@link org.hl7.fhir.r5.elementmodel} - * @param errors {@link List} - * @param stack {@link NodeStack} - */ - private void handleSpecialCaseForLastUpdated(Element bundle, List errors, NodeStack stack) { - boolean ok = bundle.hasChild(META) - && bundle.getNamedChild(META).hasChild(LAST_UPDATED) - && bundle.getNamedChild(META).getNamedChild(LAST_UPDATED).hasValue(); - rule(errors, IssueType.REQUIRED, stack.getLiteralPath(), ok, I18nConstants.DOCUMENT_DATE_REQUIRED, I18nConstants.DOCUMENT_DATE_REQUIRED_HTML); - } - - - // hack for pre-UTG v2/v3 - private boolean isV3orV2Url(String url) { - return url.startsWith("http://hl7.org/fhir/v3/") || url.startsWith("http://hl7.org/fhir/v2/"); - } - - public final static String URI_REGEX3 = "((http|https)://([A-Za-z0-9\\\\\\.\\:\\%\\$]*\\/)*)?(Account|ActivityDefinition|AllergyIntolerance|AdverseEvent|Appointment|AppointmentResponse|AuditEvent|Basic|Binary|BodySite|Bundle|CapabilityStatement|CarePlan|CareTeam|ChargeItem|Claim|ClaimResponse|ClinicalImpression|CodeSystem|Communication|CommunicationRequest|CompartmentDefinition|Composition|ConceptMap|Condition (aka Problem)|Consent|Contract|Coverage|DataElement|DetectedIssue|Device|DeviceComponent|DeviceMetric|DeviceRequest|DeviceUseStatement|DiagnosticReport|DocumentManifest|DocumentReference|EligibilityRequest|EligibilityResponse|Encounter|Endpoint|EnrollmentRequest|EnrollmentResponse|EpisodeOfCare|ExpansionProfile|ExplanationOfBenefit|FamilyMemberHistory|Flag|Goal|GraphDefinition|Group|GuidanceResponse|HealthcareService|ImagingManifest|ImagingStudy|Immunization|ImmunizationRecommendation|ImplementationGuide|Library|Linkage|List|Location|Measure|MeasureReport|Media|Medication|MedicationAdministration|MedicationDispense|MedicationRequest|MedicationStatement|MessageDefinition|MessageHeader|NamingSystem|NutritionOrder|Observation|OperationDefinition|OperationOutcome|Organization|Parameters|Patient|PaymentNotice|PaymentReconciliation|Person|PlanDefinition|Practitioner|PractitionerRole|Procedure|ProcedureRequest|ProcessRequest|ProcessResponse|Provenance|Questionnaire|QuestionnaireResponse|ReferralRequest|RelatedPerson|RequestGroup|ResearchStudy|ResearchSubject|RiskAssessment|Schedule|SearchParameter|Sequence|ServiceDefinition|Slot|Specimen|StructureDefinition|StructureMap|Subscription|Substance|SupplyDelivery|SupplyRequest|Task|TestScript|TestReport|ValueSet|VisionPrescription)\\/[A-Za-z0-9\\-\\.]{1,64}(\\/_history\\/[A-Za-z0-9\\-\\.]{1,64})?"; - private static final String EXECUTED_CONSTRAINT_LIST = "validator.executed.invariant.list"; - private static final String EXECUTION_ID = "validator.execution.id"; - - private String uriRegexForVersion() { - if (VersionUtilities.isR3Ver(context.getVersion())) - return URI_REGEX3; - else - return Constants.URI_REGEX; - } - - private String getCanonicalURLForEntry(Element entry) { - Element e = entry.getNamedChild(RESOURCE); - if (e == null) - return null; - return e.getNamedChildValue("url"); - } - - private String getIdForEntry(Element entry) { - Element e = entry.getNamedChild(RESOURCE); - if (e == null) - return null; - return e.getNamedChildValue(ID); - } - - /** - * Check each resource entry to ensure that the entry's fullURL includes the resource's id - * value. Adds an ERROR ValidationMessge to errors List for a given entry if it references - * a resource and fullURL does not include the resource's id. - * - * @param errors List of ValidationMessage objects that new errors will be added to. - * @param entries List of entry Element objects to be checked. - * @param stack Current NodeStack used to create path names in error detail messages. - */ - private void validateResourceIds(List errors, List entries, NodeStack stack) { - // TODO: Need to handle _version - int i = 1; - for (Element entry : entries) { - String fullUrl = entry.getNamedChildValue(FULL_URL); - Element resource = entry.getNamedChild(RESOURCE); - String id = resource != null ? resource.getNamedChildValue(ID) : null; - if (id != null && fullUrl != null) { - String urlId = null; - if (fullUrl.startsWith("https://") || fullUrl.startsWith("http://")) { - urlId = fullUrl.substring(fullUrl.lastIndexOf('/') + 1); - } else if (fullUrl.startsWith("urn:uuid") || fullUrl.startsWith("urn:oid")) { - urlId = fullUrl.substring(fullUrl.lastIndexOf(':') + 1); - } - rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath("entry[" + i + "]"), urlId.equals(id), I18nConstants.BUNDLE_BUNDLE_ENTRY_IDURLMISMATCH, id, fullUrl); - } - i++; - } - } - - private void checkAllInterlinked(List errors, List entries, NodeStack stack, Element bundle, boolean isError) { - List entryList = new ArrayList<>(); - for (Element entry : entries) { - Element r = entry.getNamedChild(RESOURCE); - if (r != null) { - entryList.add(new EntrySummary(entry, r)); - } - } - for (EntrySummary e : entryList) { - Set references = findReferences(e.getEntry()); - for (String ref : references) { - Element tgt = resolveInBundle(entries, ref, e.getEntry().getChildValue(FULL_URL), e.getResource().fhirType(), e.getResource().getIdBase()); - if (tgt != null) { - EntrySummary t = entryForTarget(entryList, tgt); - if (t != null) { - e.getTargets().add(t); - } - } - } - } - - Set visited = new HashSet<>(); - visitLinked(visited, entryList.get(0)); - boolean foundRevLinks; - do { - foundRevLinks = false; - for (EntrySummary e : entryList) { - if (!visited.contains(e)) { - boolean add = false; - for (EntrySummary t : e.getTargets()) { - if (visited.contains(t)) { - add = true; - } - } - if (add) { - foundRevLinks = true; - visitLinked(visited, e); - } - } - } - } while (foundRevLinks); - - int i = 0; - for (EntrySummary e : entryList) { - Element entry = e.getEntry(); - if (isError) { - rule(errors, IssueType.INFORMATIONAL, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY + '[' + (i + 1) + ']'), visited.contains(e), I18nConstants.BUNDLE_BUNDLE_ENTRY_ORPHAN, (entry.getChildValue(FULL_URL) != null ? "'" + entry.getChildValue(FULL_URL) + "'" : "")); - } else { - warning(errors, IssueType.INFORMATIONAL, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY + '[' + (i + 1) + ']'), visited.contains(e), I18nConstants.BUNDLE_BUNDLE_ENTRY_ORPHAN, (entry.getChildValue(FULL_URL) != null ? "'" + entry.getChildValue(FULL_URL) + "'" : "")); - } - i++; - } - } - - private EntrySummary entryForTarget(List entryList, Element tgt) { - for (EntrySummary e : entryList) { - if (e.getEntry() == tgt) { - return e; - } - } - return null; - } - - private void visitLinked(Set visited, EntrySummary t) { - if (!visited.contains(t)) { - visited.add(t); - for (EntrySummary e : t.getTargets()) { - visitLinked(visited, e); - } - } - } - - private void followResourceLinks(Element entry, Map visitedResources, Map candidateEntries, List candidateResources, List errors, NodeStack stack) { - followResourceLinks(entry, visitedResources, candidateEntries, candidateResources, errors, stack, 0); - } - - private void followResourceLinks(Element entry, Map visitedResources, Map candidateEntries, List candidateResources, List errors, NodeStack stack, int depth) { - Element resource = entry.getNamedChild(RESOURCE); - if (visitedResources.containsValue(resource)) - return; - - visitedResources.put(entry.getNamedChildValue(FULL_URL), resource); - - String type = null; - Set references = findReferences(resource); - for (String reference : references) { - // We don't want errors when just retrieving the element as they will be caught (with better path info) in subsequent processing - IndexedElement r = getFromBundle(stack.getElement(), reference, entry.getChildValue(FULL_URL), new ArrayList(), stack.addToLiteralPath("entry[" + candidateResources.indexOf(resource) + "]"), type, "transaction".equals(stack.getElement().getChildValue(TYPE))); - if (r != null && !visitedResources.containsValue(r.getMatch())) { - followResourceLinks(candidateEntries.get(r.getMatch()), visitedResources, candidateEntries, candidateResources, errors, stack, depth + 1); - } - } - } - - private Set findReferences(Element start) { - Set references = new HashSet(); - findReferences(start, references); - return references; - } - - private void findReferences(Element start, Set references) { - for (Element child : start.getChildren()) { - if (child.getType().equals("Reference")) { - String ref = child.getChildValue("reference"); - if (ref != null && !ref.startsWith("#")) - references.add(ref); - } - if (child.getType().equals("url") || child.getType().equals("uri") || child.getType().equals("canonical")) { - String ref = child.primitiveValue(); - if (ref != null && !ref.startsWith("#")) - references.add(ref); - } - findReferences(child, references); - } - } - - private void validateBundleReference(List errors, List entries, Element ref, String name, NodeStack stack, String fullUrl, String type, String id) { - String reference = null; - try { - reference = ref.getNamedChildValue("reference"); - } catch (Error e) { - - } - - if (ref != null && !Utilities.noString(reference) && !reference.startsWith("#")) { - Element target = resolveInBundle(entries, reference, fullUrl, type, id); - rule(errors, IssueType.INVALID, ref.line(), ref.col(), stack.addToLiteralPath("reference"), target != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOTFOUND, reference, name); - } - } - + private void validateContains(ValidatorHostContext hostContext, List errors, String path, ElementDefinition child, ElementDefinition context, Element resource, Element element, NodeStack stack, IdStatus idstatus) throws FHIRException { String resourceName = element.getType(); TypeRefComponent trr = null; @@ -3919,50 +3488,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return false; } - 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)) { - - // the composition subject etc references must resolve in the bundle - validateDocumentReference(errors, entries, composition, stack, fullUrl, id, false, "subject", "Composition"); - validateDocumentReference(errors, entries, composition, stack, fullUrl, id, true, "author", "Composition"); - validateDocumentReference(errors, entries, composition, stack, fullUrl, id, false, "encounter", "Composition"); - validateDocumentReference(errors, entries, composition, stack, fullUrl, id, false, "custodian", "Composition"); - validateDocumentSubReference(errors, entries, composition, stack, fullUrl, id, "Composition", "attester", false, "party"); - validateDocumentSubReference(errors, entries, composition, stack, fullUrl, id, "Composition", "event", true, "detail"); - - validateSections(errors, entries, composition, stack, fullUrl, id); - } - } - - public void validateDocumentSubReference(List errors, List entries, Element composition, NodeStack stack, String fullUrl, String id, String title, String parent, boolean repeats, String propName) { - List list = new ArrayList<>(); - composition.getNamedChildren(parent, list); - int i = 1; - for (Element elem : list) { - validateDocumentReference(errors, entries, elem, stack.push(elem, i, null, null), fullUrl, id, repeats, propName, title + "." + parent); - i++; - } - } - - public void validateDocumentReference(List errors, List entries, Element composition, NodeStack stack, String fullUrl, String id, boolean repeats, String propName, String title) { - if (repeats) { - List list = new ArrayList<>(); - composition.getNamedChildren(propName, list); - int i = 1; - for (Element elem : list) { - - validateBundleReference(errors, entries, elem, title + "." + propName, stack.push(elem, i, null, null), fullUrl, "Composition", id); - i++; - } - - } else { - Element elem = composition.getNamedChild(propName); - if (elem != null) { - validateBundleReference(errors, entries, elem, title + "." + propName, stack.push(elem, -1, null, null), fullUrl, "Composition", id); - } - } - } private void validateElement(ValidatorHostContext hostContext, List errors, StructureDefinition profile, ElementDefinition definition, StructureDefinition cprofile, ElementDefinition context, Element resource, Element element, String actualType, NodeStack stack, boolean inCodeableConcept, boolean checkDisplayInContext, String extensionUrl) throws FHIRException { @@ -4668,16 +4193,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } } } - - private void validateMessage(List errors, List entries, Element messageHeader, NodeStack stack, String fullUrl, String id) { - // first entry must be a messageheader - if (rule(errors, IssueType.INVALID, messageHeader.line(), messageHeader.col(), stack.getLiteralPath(), messageHeader.getType().equals("MessageHeader"), I18nConstants.VALIDATION_BUNDLE_MESSAGE)) { - List elements = messageHeader.getChildren("focus"); - for (Element elem : elements) - validateBundleReference(errors, entries, elem, "MessageHeader Data", stack.push(elem, -1, null, null), fullUrl, "MessageHeader", id); - } - } - + private void validateObservation(List errors, Element element, NodeStack stack) { // all observations should have a subject, a performer, and a time @@ -4742,30 +4258,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } } - private void validateSections(List errors, List entries, Element focus, NodeStack stack, String fullUrl, String id) { - List sections = new ArrayList(); - focus.getNamedChildren("section", sections); - int i = 1; - for (Element section : sections) { - NodeStack localStack = stack.push(section, i, null, null); - - // technically R4+, but there won't be matches from before that - validateDocumentReference(errors, entries, section, stack, fullUrl, id, false, "author", "Section"); - validateDocumentReference(errors, entries, section, stack, fullUrl, id, false, "focus", "Section"); - - List sectionEntries = new ArrayList(); - section.getNamedChildren(ENTRY, sectionEntries); - int j = 1; - for (Element sectionEntry : sectionEntries) { - NodeStack localStack2 = localStack.push(sectionEntry, j, null, null); - validateBundleReference(errors, entries, sectionEntry, "Section Entry", localStack2, fullUrl, "Composition", id); - j++; - } - validateSections(errors, entries, section, localStack, fullUrl, id); - i++; - } - } - private boolean valueMatchesCriteria(Element value, ElementDefinition criteria, StructureDefinition profile) throws FHIRException { if (criteria.hasFixed()) { List msgs = new ArrayList(); 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 new file mode 100644 index 000000000..a46de6267 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/BundleValidator.java @@ -0,0 +1,375 @@ +package org.hl7.fhir.validation.instance.type; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.elementmodel.Element; +import org.hl7.fhir.r5.model.Constants; +import org.hl7.fhir.r5.model.Enumerations.FHIRVersion; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.VersionUtilities; +import org.hl7.fhir.utilities.i18n.I18nConstants; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; +import org.hl7.fhir.validation.BaseValidator; +import org.hl7.fhir.validation.instance.utils.EntrySummary; +import org.hl7.fhir.validation.instance.utils.IndexedElement; +import org.hl7.fhir.validation.instance.utils.NodeStack; + +public class BundleValidator extends BaseValidator{ + public final static String URI_REGEX3 = "((http|https)://([A-Za-z0-9\\\\\\.\\:\\%\\$]*\\/)*)?(Account|ActivityDefinition|AllergyIntolerance|AdverseEvent|Appointment|AppointmentResponse|AuditEvent|Basic|Binary|BodySite|Bundle|CapabilityStatement|CarePlan|CareTeam|ChargeItem|Claim|ClaimResponse|ClinicalImpression|CodeSystem|Communication|CommunicationRequest|CompartmentDefinition|Composition|ConceptMap|Condition (aka Problem)|Consent|Contract|Coverage|DataElement|DetectedIssue|Device|DeviceComponent|DeviceMetric|DeviceRequest|DeviceUseStatement|DiagnosticReport|DocumentManifest|DocumentReference|EligibilityRequest|EligibilityResponse|Encounter|Endpoint|EnrollmentRequest|EnrollmentResponse|EpisodeOfCare|ExpansionProfile|ExplanationOfBenefit|FamilyMemberHistory|Flag|Goal|GraphDefinition|Group|GuidanceResponse|HealthcareService|ImagingManifest|ImagingStudy|Immunization|ImmunizationRecommendation|ImplementationGuide|Library|Linkage|List|Location|Measure|MeasureReport|Media|Medication|MedicationAdministration|MedicationDispense|MedicationRequest|MedicationStatement|MessageDefinition|MessageHeader|NamingSystem|NutritionOrder|Observation|OperationDefinition|OperationOutcome|Organization|Parameters|Patient|PaymentNotice|PaymentReconciliation|Person|PlanDefinition|Practitioner|PractitionerRole|Procedure|ProcedureRequest|ProcessRequest|ProcessResponse|Provenance|Questionnaire|QuestionnaireResponse|ReferralRequest|RelatedPerson|RequestGroup|ResearchStudy|ResearchSubject|RiskAssessment|Schedule|SearchParameter|Sequence|ServiceDefinition|Slot|Specimen|StructureDefinition|StructureMap|Subscription|Substance|SupplyDelivery|SupplyRequest|Task|TestScript|TestReport|ValueSet|VisionPrescription)\\/[A-Za-z0-9\\-\\.]{1,64}(\\/_history\\/[A-Za-z0-9\\-\\.]{1,64})?"; + private String serverBase; + + public BundleValidator(IWorkerContext context, String serverBase) { + super(context); + this.serverBase = serverBase; + } + + public void validateBundle(List errors, Element bundle, NodeStack stack, boolean checkSpecials) { + List entries = new ArrayList(); + bundle.getNamedChildren(ENTRY, entries); + String type = bundle.getNamedChildValue(TYPE); + type = StringUtils.defaultString(type); + + if (entries.size() == 0) { + rule(errors, IssueType.INVALID, stack.getLiteralPath(), !(type.equals(DOCUMENT) || type.equals(MESSAGE)), I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRST); + } else { + // Get the first entry, the MessageHeader + Element firstEntry = entries.get(0); + // Get the stack of the first entry + NodeStack firstStack = stack.push(firstEntry, 1, null, null); + + String fullUrl = firstEntry.getNamedChildValue(FULL_URL); + + if (type.equals(DOCUMENT)) { + Element resource = firstEntry.getNamedChild(RESOURCE); + if (rule(errors, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), resource != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRSTRESOURCE)) { + String id = resource.getNamedChildValue(ID); + validateDocument(errors, entries, resource, firstStack.push(resource, -1, null, null), fullUrl, id); + } + if (!VersionUtilities.isThisOrLater(FHIRVersion._4_0_1.getDisplay(), bundle.getProperty().getStructure().getFhirVersion().getDisplay())) { + handleSpecialCaseForLastUpdated(bundle, errors, stack); + } + checkAllInterlinked(errors, entries, stack, bundle, true); + } + if (type.equals(MESSAGE)) { + Element resource = firstEntry.getNamedChild(RESOURCE); + String id = resource.getNamedChildValue(ID); + if (rule(errors, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), resource != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRSTRESOURCE)) { + validateMessage(errors, entries, resource, firstStack.push(resource, -1, null, null), fullUrl, id); + } + checkAllInterlinked(errors, entries, stack, bundle, VersionUtilities.isR5Ver(context.getVersion())); + } + // We do not yet have rules requiring that the id and fullUrl match when dealing with messaging Bundles + // validateResourceIds(errors, entries, stack); + } + for (Element entry : entries) { + String fullUrl = entry.getNamedChildValue(FULL_URL); + String url = getCanonicalURLForEntry(entry); + String id = getIdForEntry(entry); + if (url != null) { + if (!(!url.equals(fullUrl) || (url.matches(uriRegexForVersion()) && url.endsWith("/" + id))) && !isV3orV2Url(url)) + rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), false, I18nConstants.BUNDLE_BUNDLE_ENTRY_MISMATCHIDURL, url, fullUrl, id); + rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), !url.equals(fullUrl) || serverBase == null || (url.equals(Utilities.pathURL(serverBase, entry.getNamedChild(RESOURCE).fhirType(), id))), I18nConstants.BUNDLE_BUNDLE_ENTRY_CANONICAL, url, fullUrl); + } + // todo: check specials + } + } + + 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)) { + + // the composition subject etc references must resolve in the bundle + validateDocumentReference(errors, entries, composition, stack, fullUrl, id, false, "subject", "Composition"); + validateDocumentReference(errors, entries, composition, stack, fullUrl, id, true, "author", "Composition"); + validateDocumentReference(errors, entries, composition, stack, fullUrl, id, false, "encounter", "Composition"); + validateDocumentReference(errors, entries, composition, stack, fullUrl, id, false, "custodian", "Composition"); + validateDocumentSubReference(errors, entries, composition, stack, fullUrl, id, "Composition", "attester", false, "party"); + validateDocumentSubReference(errors, entries, composition, stack, fullUrl, id, "Composition", "event", true, "detail"); + + validateSections(errors, entries, composition, stack, fullUrl, id); + } + } + + private void validateSections(List errors, List entries, Element focus, NodeStack stack, String fullUrl, String id) { + List sections = new ArrayList(); + focus.getNamedChildren("section", sections); + int i = 1; + for (Element section : sections) { + NodeStack localStack = stack.push(section, i, null, null); + + // technically R4+, but there won't be matches from before that + validateDocumentReference(errors, entries, section, stack, fullUrl, id, false, "author", "Section"); + validateDocumentReference(errors, entries, section, stack, fullUrl, id, false, "focus", "Section"); + + List sectionEntries = new ArrayList(); + section.getNamedChildren(ENTRY, sectionEntries); + int j = 1; + for (Element sectionEntry : sectionEntries) { + NodeStack localStack2 = localStack.push(sectionEntry, j, null, null); + validateBundleReference(errors, entries, sectionEntry, "Section Entry", localStack2, fullUrl, "Composition", id); + j++; + } + validateSections(errors, entries, section, localStack, fullUrl, id); + i++; + } + } + + + public void validateDocumentSubReference(List errors, List entries, Element composition, NodeStack stack, String fullUrl, String id, String title, String parent, boolean repeats, String propName) { + List list = new ArrayList<>(); + composition.getNamedChildren(parent, list); + int i = 1; + for (Element elem : list) { + validateDocumentReference(errors, entries, elem, stack.push(elem, i, null, null), fullUrl, id, repeats, propName, title + "." + parent); + i++; + } + } + + public void validateDocumentReference(List errors, List entries, Element composition, NodeStack stack, String fullUrl, String id, boolean repeats, String propName, String title) { + if (repeats) { + List list = new ArrayList<>(); + composition.getNamedChildren(propName, list); + int i = 1; + for (Element elem : list) { + + validateBundleReference(errors, entries, elem, title + "." + propName, stack.push(elem, i, null, null), fullUrl, "Composition", id); + i++; + } + + } else { + Element elem = composition.getNamedChild(propName); + if (elem != null) { + validateBundleReference(errors, entries, elem, title + "." + propName, stack.push(elem, -1, null, null), fullUrl, "Composition", id); + } + } + } + + private void validateMessage(List errors, List entries, Element messageHeader, NodeStack stack, String fullUrl, String id) { + // first entry must be a messageheader + if (rule(errors, IssueType.INVALID, messageHeader.line(), messageHeader.col(), stack.getLiteralPath(), messageHeader.getType().equals("MessageHeader"), I18nConstants.VALIDATION_BUNDLE_MESSAGE)) { + List elements = messageHeader.getChildren("focus"); + for (Element elem : elements) + validateBundleReference(errors, entries, elem, "MessageHeader Data", stack.push(elem, -1, null, null), fullUrl, "MessageHeader", id); + } + } + + private void validateBundleReference(List errors, List entries, Element ref, String name, NodeStack stack, String fullUrl, String type, String id) { + String reference = null; + try { + reference = ref.getNamedChildValue("reference"); + } catch (Error e) { + + } + + if (ref != null && !Utilities.noString(reference) && !reference.startsWith("#")) { + Element target = resolveInBundle(entries, reference, fullUrl, type, id); + rule(errors, IssueType.INVALID, ref.line(), ref.col(), stack.addToLiteralPath("reference"), target != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOTFOUND, reference, name); + } + } + + + /** + * As per outline for Document Content: + *
  • "The document date (mandatory). This is found in Bundle.meta.lastUpdated and identifies when the document bundle + * was assembled from the underlying resources"
  • + *

    + * This check was not being done for release versions < r4. + *

    + * Related JIRA ticket is FHIR-26544 + * + * @param bundle {@link org.hl7.fhir.r5.elementmodel} + * @param errors {@link List} + * @param stack {@link NodeStack} + */ + private void handleSpecialCaseForLastUpdated(Element bundle, List errors, NodeStack stack) { + boolean ok = bundle.hasChild(META) + && bundle.getNamedChild(META).hasChild(LAST_UPDATED) + && bundle.getNamedChild(META).getNamedChild(LAST_UPDATED).hasValue(); + rule(errors, IssueType.REQUIRED, stack.getLiteralPath(), ok, I18nConstants.DOCUMENT_DATE_REQUIRED, I18nConstants.DOCUMENT_DATE_REQUIRED_HTML); + } + + private void checkAllInterlinked(List errors, List entries, NodeStack stack, Element bundle, boolean isError) { + List entryList = new ArrayList<>(); + for (Element entry : entries) { + Element r = entry.getNamedChild(RESOURCE); + if (r != null) { + entryList.add(new EntrySummary(entry, r)); + } + } + for (EntrySummary e : entryList) { + Set references = findReferences(e.getEntry()); + for (String ref : references) { + Element tgt = resolveInBundle(entries, ref, e.getEntry().getChildValue(FULL_URL), e.getResource().fhirType(), e.getResource().getIdBase()); + if (tgt != null) { + EntrySummary t = entryForTarget(entryList, tgt); + if (t != null) { + e.getTargets().add(t); + } + } + } + } + + Set visited = new HashSet<>(); + visitLinked(visited, entryList.get(0)); + boolean foundRevLinks; + do { + foundRevLinks = false; + for (EntrySummary e : entryList) { + if (!visited.contains(e)) { + boolean add = false; + for (EntrySummary t : e.getTargets()) { + if (visited.contains(t)) { + add = true; + } + } + if (add) { + foundRevLinks = true; + visitLinked(visited, e); + } + } + } + } while (foundRevLinks); + + int i = 0; + for (EntrySummary e : entryList) { + Element entry = e.getEntry(); + if (isError) { + rule(errors, IssueType.INFORMATIONAL, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY + '[' + (i + 1) + ']'), visited.contains(e), I18nConstants.BUNDLE_BUNDLE_ENTRY_ORPHAN, (entry.getChildValue(FULL_URL) != null ? "'" + entry.getChildValue(FULL_URL) + "'" : "")); + } else { + warning(errors, IssueType.INFORMATIONAL, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY + '[' + (i + 1) + ']'), visited.contains(e), I18nConstants.BUNDLE_BUNDLE_ENTRY_ORPHAN, (entry.getChildValue(FULL_URL) != null ? "'" + entry.getChildValue(FULL_URL) + "'" : "")); + } + i++; + } + } + + + + private String uriRegexForVersion() { + if (VersionUtilities.isR3Ver(context.getVersion())) + return URI_REGEX3; + else + return Constants.URI_REGEX; + } + + private String getCanonicalURLForEntry(Element entry) { + Element e = entry.getNamedChild(RESOURCE); + if (e == null) + return null; + return e.getNamedChildValue("url"); + } + + private String getIdForEntry(Element entry) { + Element e = entry.getNamedChild(RESOURCE); + if (e == null) + return null; + return e.getNamedChildValue(ID); + } + + /** + * Check each resource entry to ensure that the entry's fullURL includes the resource's id + * value. Adds an ERROR ValidationMessge to errors List for a given entry if it references + * a resource and fullURL does not include the resource's id. + * + * @param errors List of ValidationMessage objects that new errors will be added to. + * @param entries List of entry Element objects to be checked. + * @param stack Current NodeStack used to create path names in error detail messages. + */ + private void validateResourceIds(List errors, List entries, NodeStack stack) { + // TODO: Need to handle _version + int i = 1; + for (Element entry : entries) { + String fullUrl = entry.getNamedChildValue(FULL_URL); + Element resource = entry.getNamedChild(RESOURCE); + String id = resource != null ? resource.getNamedChildValue(ID) : null; + if (id != null && fullUrl != null) { + String urlId = null; + if (fullUrl.startsWith("https://") || fullUrl.startsWith("http://")) { + urlId = fullUrl.substring(fullUrl.lastIndexOf('/') + 1); + } else if (fullUrl.startsWith("urn:uuid") || fullUrl.startsWith("urn:oid")) { + urlId = fullUrl.substring(fullUrl.lastIndexOf(':') + 1); + } + rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath("entry[" + i + "]"), urlId.equals(id), I18nConstants.BUNDLE_BUNDLE_ENTRY_IDURLMISMATCH, id, fullUrl); + } + i++; + } + } + + private EntrySummary entryForTarget(List entryList, Element tgt) { + for (EntrySummary e : entryList) { + if (e.getEntry() == tgt) { + return e; + } + } + return null; + } + + private void visitLinked(Set visited, EntrySummary t) { + if (!visited.contains(t)) { + visited.add(t); + for (EntrySummary e : t.getTargets()) { + visitLinked(visited, e); + } + } + } + + private void followResourceLinks(Element entry, Map visitedResources, Map candidateEntries, List candidateResources, List errors, NodeStack stack) { + followResourceLinks(entry, visitedResources, candidateEntries, candidateResources, errors, stack, 0); + } + + private void followResourceLinks(Element entry, Map visitedResources, Map candidateEntries, List candidateResources, List errors, NodeStack stack, int depth) { + Element resource = entry.getNamedChild(RESOURCE); + if (visitedResources.containsValue(resource)) + return; + + visitedResources.put(entry.getNamedChildValue(FULL_URL), resource); + + String type = null; + Set references = findReferences(resource); + for (String reference : references) { + // We don't want errors when just retrieving the element as they will be caught (with better path info) in subsequent processing + IndexedElement r = getFromBundle(stack.getElement(), reference, entry.getChildValue(FULL_URL), new ArrayList(), stack.addToLiteralPath("entry[" + candidateResources.indexOf(resource) + "]"), type, "transaction".equals(stack.getElement().getChildValue(TYPE))); + if (r != null && !visitedResources.containsValue(r.getMatch())) { + followResourceLinks(candidateEntries.get(r.getMatch()), visitedResources, candidateEntries, candidateResources, errors, stack, depth + 1); + } + } + } + + + private Set findReferences(Element start) { + Set references = new HashSet(); + findReferences(start, references); + return references; + } + + private void findReferences(Element start, Set references) { + for (Element child : start.getChildren()) { + if (child.getType().equals("Reference")) { + String ref = child.getChildValue("reference"); + if (ref != null && !ref.startsWith("#")) + references.add(ref); + } + if (child.getType().equals("url") || child.getType().equals("uri") || child.getType().equals("canonical")) { + String ref = child.primitiveValue(); + if (ref != null && !ref.startsWith("#")) + references.add(ref); + } + findReferences(child, references); + } + } + + + + // hack for pre-UTG v2/v3 + private boolean isV3orV2Url(String url) { + return url.startsWith("http://hl7.org/fhir/v3/") || url.startsWith("http://hl7.org/fhir/v2/"); + } + + +} diff --git a/release-notes-validator.md b/release-notes-validator.md index c30fc5d63..15e7000cc 100644 --- a/release-notes-validator.md +++ b/release-notes-validator.md @@ -6,11 +6,11 @@ title: FHIR Validator Release Notes ## Current (not released yet) -(no changes yet) +* fix fatal NPE validating bundles when resource is missing +* fix tests for R5 changes ## v4.2.30 (2020-05-12) - * Allow mailto: urls as absolute URIs ## v4.2.29 (2020-05-11)