001package org.hl7.fhir.validation.instance.type; 002 003import java.util.ArrayList; 004import java.util.HashMap; 005import java.util.HashSet; 006import java.util.List; 007import java.util.Map; 008import java.util.Set; 009 010import org.apache.commons.lang3.StringUtils; 011import org.hl7.fhir.r5.context.IWorkerContext; 012import org.hl7.fhir.r5.elementmodel.Element; 013import org.hl7.fhir.r5.model.Constants; 014import org.hl7.fhir.r5.model.Enumerations.FHIRVersion; 015import org.hl7.fhir.r5.model.StructureDefinition; 016import org.hl7.fhir.r5.utils.XVerExtensionManager; 017import org.hl7.fhir.r5.utils.validation.BundleValidationRule; 018import org.hl7.fhir.utilities.Utilities; 019import org.hl7.fhir.utilities.VersionUtilities; 020import org.hl7.fhir.utilities.i18n.I18nConstants; 021import org.hl7.fhir.utilities.validation.ValidationMessage; 022import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 023import org.hl7.fhir.validation.BaseValidator; 024import org.hl7.fhir.validation.instance.InstanceValidator; 025import org.hl7.fhir.validation.instance.utils.EntrySummary; 026import org.hl7.fhir.validation.instance.utils.IndexedElement; 027import org.hl7.fhir.validation.instance.utils.NodeStack; 028import org.hl7.fhir.validation.instance.utils.ValidatorHostContext; 029 030public class BundleValidator extends BaseValidator { 031 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})?"; 032 private String serverBase; 033 private InstanceValidator validator; 034 035 public BundleValidator(IWorkerContext context, String serverBase, InstanceValidator validator, XVerExtensionManager xverManager) { 036 super(context, xverManager); 037 this.serverBase = serverBase; 038 this.validator = validator; 039 } 040 041 public void validateBundle(List<ValidationMessage> errors, Element bundle, NodeStack stack, boolean checkSpecials, ValidatorHostContext hostContext) { 042 List<Element> entries = new ArrayList<Element>(); 043 bundle.getNamedChildren(ENTRY, entries); 044 String type = bundle.getNamedChildValue(TYPE); 045 type = StringUtils.defaultString(type); 046 047 if (entries.size() == 0) { 048 rule(errors, IssueType.INVALID, stack.getLiteralPath(), !(type.equals(DOCUMENT) || type.equals(MESSAGE)), I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRST); 049 } else { 050 // Get the first entry, the MessageHeader 051 Element firstEntry = entries.get(0); 052 // Get the stack of the first entry 053 NodeStack firstStack = stack.push(firstEntry, 1, null, null); 054 055 String fullUrl = firstEntry.getNamedChildValue(FULL_URL); 056 057 if (type.equals(DOCUMENT)) { 058 Element resource = firstEntry.getNamedChild(RESOURCE); 059 if (rule(errors, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), resource != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRSTRESOURCE)) { 060 String id = resource.getNamedChildValue(ID); 061 validateDocument(errors, entries, resource, firstStack.push(resource, -1, null, null), fullUrl, id); 062 } 063 if (!VersionUtilities.isThisOrLater(FHIRVersion._4_0_1.getDisplay(), bundle.getProperty().getStructure().getFhirVersion().getDisplay())) { 064 handleSpecialCaseForLastUpdated(bundle, errors, stack); 065 } 066 checkAllInterlinked(errors, entries, stack, bundle, true); 067 } 068 if (type.equals(MESSAGE)) { 069 Element resource = firstEntry.getNamedChild(RESOURCE); 070 String id = resource.getNamedChildValue(ID); 071 if (rule(errors, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), resource != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRSTRESOURCE)) { 072 validateMessage(errors, entries, resource, firstStack.push(resource, -1, null, null), fullUrl, id); 073 } 074 checkAllInterlinked(errors, entries, stack, bundle, VersionUtilities.isR5Ver(context.getVersion())); 075 } 076 if (type.equals(SEARCHSET)) { 077 checkSearchSet(errors, bundle, entries, stack); 078 } 079 // We do not yet have rules requiring that the id and fullUrl match when dealing with messaging Bundles 080 // validateResourceIds(errors, entries, stack); 081 } 082 083 int count = 0; 084 Map<String, Integer> counter = new HashMap<>(); 085 086 boolean fullUrlOptional = Utilities.existsInList(type, "transaction", "transaction-response", "batch", "batch-response"); 087 088 for (Element entry : entries) { 089 NodeStack estack = stack.push(entry, count, null, null); 090 String fullUrl = entry.getNamedChildValue(FULL_URL); 091 String url = getCanonicalURLForEntry(entry); 092 String id = getIdForEntry(entry); 093 if (url != null) { 094 if (!(!url.equals(fullUrl) || (url.matches(uriRegexForVersion()) && url.endsWith("/" + id))) && !isV3orV2Url(url)) 095 rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), false, I18nConstants.BUNDLE_BUNDLE_ENTRY_MISMATCHIDURL, url, fullUrl, id); 096 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); 097 } 098 099 if (!VersionUtilities.isR2Ver(context.getVersion())) { 100 rule(errors, IssueType.INVALID, entry.line(), entry.col(), estack.getLiteralPath(), fullUrlOptional || fullUrl != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_FULLURL_REQUIRED); 101 } 102 // check bundle profile requests 103 if (entry.hasChild(RESOURCE)) { 104 String rtype = entry.getNamedChild(RESOURCE).fhirType(); 105 int rcount = counter.containsKey(rtype) ? counter.get(rtype)+1 : 0; 106 counter.put(rtype, rcount); 107 for (BundleValidationRule bvr : validator.getBundleValidationRules()) { 108 if (meetsRule(bvr, rtype, rcount, count)) { 109 StructureDefinition defn = validator.getContext().fetchResource(StructureDefinition.class, bvr.getProfile()); 110 if (defn == null) { 111 throw new Error(validator.getContext().formatMessage(I18nConstants.BUNDLE_RULE_PROFILE_UNKNOWN, bvr.getRule(), bvr.getProfile())); 112 } else { 113 Element res = entry.getNamedChild(RESOURCE); 114 NodeStack rstack = estack.push(res, -1, null, null); 115 if (validator.isCrumbTrails()) { 116 res.addMessage(signpost(errors, IssueType.INFORMATIONAL, res.line(), res.col(), stack.getLiteralPath(), I18nConstants.VALIDATION_VAL_PROFILE_SIGNPOST_BUNDLE_PARAM, defn.getUrl())); 117 } 118 stack.resetIds(); 119 validator.startInner(hostContext, errors, res, res, defn, rstack, false); 120 } 121 } 122 } 123 } 124 125 // todo: check specials 126 count++; 127 } 128 } 129 130 private void checkSearchSet(List<ValidationMessage> errors, Element bundle, List<Element> entries, NodeStack stack) { 131 // warning: should have self link 132 List<Element> links = new ArrayList<Element>(); 133 bundle.getNamedChildren(LINK, links); 134 Element selfLink = getSelfLink(links); 135 List<String> types = new ArrayList<>(); 136 if (selfLink == null) { 137 warning(errors, IssueType.INVALID, bundle.line(), bundle.col(), stack.getLiteralPath(), false, I18nConstants.BUNDLE_SEARCH_NOSELF); 138 } else { 139 readSearchResourceTypes(selfLink.getNamedChildValue("url"), types); 140 if (types.size() == 0) { 141 hint(errors, IssueType.INVALID, bundle.line(), bundle.col(), stack.getLiteralPath(), false, I18nConstants.BUNDLE_SEARCH_SELF_NOT_UNDERSTOOD); 142 } 143 } 144 145 Boolean searchMode = readHasSearchMode(entries); 146 if (searchMode != null && searchMode == false) { // if no resources have search mode 147 boolean typeProblem = false; 148 String rtype = null; 149 int count = 0; 150 for (Element entry : entries) { 151 NodeStack estack = stack.push(entry, count, null, null); 152 count++; 153 Element res = entry.getNamedChild("resource"); 154 if (rule(errors, IssueType.INVALID, bundle.line(), bundle.col(), estack.getLiteralPath(), res != null, I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE)) { 155 NodeStack rstack = estack.push(res, -1, null, null); 156 String rt = res.fhirType(); 157 Boolean ok = checkSearchType(types, rt); 158 if (ok == null) { 159 typeProblem = true; 160 hint(errors, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), selfLink == null, I18nConstants.BUNDLE_SEARCH_ENTRY_TYPE_NOT_SURE); 161 String id = res.getNamedChildValue("id"); 162 warning(errors, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), id != null || "OperationOutcome".equals(rt), I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE_ID); 163 } else if (ok) { 164 if (!"OperationOutcome".equals(rt)) { 165 String id = res.getNamedChildValue("id"); 166 warning(errors, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), id != null, I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE_ID); 167 if (rtype != null && !rt.equals(rtype)) { 168 typeProblem = true; 169 } else if (rtype == null) { 170 rtype = rt; 171 } 172 } 173 } else { 174 typeProblem = true; 175 warning(errors, IssueType.INVALID, bundle.line(), bundle.col(), estack.getLiteralPath(), false, I18nConstants.BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_NO_MODE, rt, types); 176 } 177 } 178 } 179 if (typeProblem) { 180 warning(errors, IssueType.INVALID, bundle.line(), bundle.col(), stack.getLiteralPath(), !typeProblem, I18nConstants.BUNDLE_SEARCH_NO_MODE); 181 } else { 182 hint(errors, IssueType.INVALID, bundle.line(), bundle.col(), stack.getLiteralPath(), !typeProblem, I18nConstants.BUNDLE_SEARCH_NO_MODE); 183 } 184 } else { 185 int count = 0; 186 for (Element entry : entries) { 187 NodeStack estack = stack.push(entry, count, null, null); 188 count++; 189 Element res = entry.getNamedChild("resource"); 190 String sm = null; 191 Element s = entry.getNamedChild("search"); 192 if (s != null) { 193 sm = s.getNamedChildValue("mode"); 194 } 195 warning(errors, IssueType.INVALID, bundle.line(), bundle.col(), estack.getLiteralPath(), sm != null, I18nConstants.BUNDLE_SEARCH_NO_MODE); 196 if (rule(errors, IssueType.INVALID, bundle.line(), bundle.col(), estack.getLiteralPath(), res != null, I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE)) { 197 NodeStack rstack = estack.push(res, -1, null, null); 198 String rt = res.fhirType(); 199 String id = res.getNamedChildValue("id"); 200 if (sm != null) { 201 if ("match".equals(sm)) { 202 rule(errors, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), id != null, I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE_ID); 203 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); 204 } else if ("include".equals(sm)) { 205 rule(errors, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), id != null, I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE_ID); 206 } else { // outcome 207 rule(errors, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), "OperationOutcome".equals(rt), I18nConstants.BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_OUTCOME, rt); 208 } 209 } 210 } 211 } 212 } 213 } 214 215 private Boolean checkSearchType(List<String> types, String rt) { 216 if (types.size() == 0) { 217 return null; 218 } else { 219 return Utilities.existsInList(rt, types); 220 } 221 } 222 223 private Boolean readHasSearchMode(List<Element> entries) { 224 boolean all = true; 225 boolean any = false; 226 for (Element entry : entries) { 227 String sm = null; 228 Element s = entry.getNamedChild("search"); 229 if (s != null) { 230 sm = s.getNamedChildValue("mode"); 231 } 232 if (sm != null) { 233 any = true; 234 } else { 235 all = false; 236 } 237 } 238 if (all) { 239 return true; 240 } else if (any) { 241 return null; 242 } else { 243 return false; 244 } 245 } 246 247 private void readSearchResourceTypes(String ref, List<String> types) { 248 if (ref == null) { 249 return; 250 } 251 String[] head = null; 252 String[] tail = null; 253 if (ref.contains("?")) { 254 head = ref.substring(0, ref.indexOf("?")).split("\\/"); 255 tail = ref.substring(ref.indexOf("?")+1).split("\\&"); 256 } else { 257 head = ref.split("\\/"); 258 } 259 if (head == null || head.length == 0) { 260 return; 261 } else if (context.getResourceNames().contains(head[head.length-1])) { 262 types.add(head[head.length-1]); 263 } else if (tail != null) { 264 for (String s : tail) { 265 if (s.startsWith("_type=")) { 266 for (String t : s.substring(6).split("\\,")) { 267 types.add(t); 268 } 269 } 270 } 271 } 272 } 273 274 private Element getSelfLink(List<Element> links) { 275 for (Element link : links) { 276 if ("self".equals(link.getNamedChildValue("relation"))) { 277 return link; 278 } 279 } 280 return null; 281 } 282 283 private void validateDocument(List<ValidationMessage> errors, List<Element> entries, Element composition, NodeStack stack, String fullUrl, String id) { 284 // first entry must be a composition 285 if (rule(errors, IssueType.INVALID, composition.line(), composition.col(), stack.getLiteralPath(), composition.getType().equals("Composition"), I18nConstants.BUNDLE_BUNDLE_ENTRY_DOCUMENT)) { 286 287 // the composition subject etc references must resolve in the bundle 288 validateDocumentReference(errors, entries, composition, stack, fullUrl, id, false, "subject", "Composition"); 289 validateDocumentReference(errors, entries, composition, stack, fullUrl, id, true, "author", "Composition"); 290 validateDocumentReference(errors, entries, composition, stack, fullUrl, id, false, "encounter", "Composition"); 291 validateDocumentReference(errors, entries, composition, stack, fullUrl, id, false, "custodian", "Composition"); 292 validateDocumentSubReference(errors, entries, composition, stack, fullUrl, id, "Composition", "attester", false, "party"); 293 validateDocumentSubReference(errors, entries, composition, stack, fullUrl, id, "Composition", "event", true, "detail"); 294 295 validateSections(errors, entries, composition, stack, fullUrl, id); 296 } 297 } 298 299 private void validateSections(List<ValidationMessage> errors, List<Element> entries, Element focus, NodeStack stack, String fullUrl, String id) { 300 List<Element> sections = new ArrayList<Element>(); 301 focus.getNamedChildren("section", sections); 302 int i = 1; 303 for (Element section : sections) { 304 NodeStack localStack = stack.push(section, i, null, null); 305 306 // technically R4+, but there won't be matches from before that 307 validateDocumentReference(errors, entries, section, stack, fullUrl, id, true, "author", "Section"); 308 validateDocumentReference(errors, entries, section, stack, fullUrl, id, false, "focus", "Section"); 309 310 List<Element> sectionEntries = new ArrayList<Element>(); 311 section.getNamedChildren(ENTRY, sectionEntries); 312 int j = 1; 313 for (Element sectionEntry : sectionEntries) { 314 NodeStack localStack2 = localStack.push(sectionEntry, j, null, null); 315 validateBundleReference(errors, entries, sectionEntry, "Section Entry", localStack2, fullUrl, "Composition", id); 316 j++; 317 } 318 validateSections(errors, entries, section, localStack, fullUrl, id); 319 i++; 320 } 321 } 322 323 324 public void validateDocumentSubReference(List<ValidationMessage> errors, List<Element> entries, Element composition, NodeStack stack, String fullUrl, String id, String title, String parent, boolean repeats, String propName) { 325 List<Element> list = new ArrayList<>(); 326 composition.getNamedChildren(parent, list); 327 int i = 1; 328 for (Element elem : list) { 329 validateDocumentReference(errors, entries, elem, stack.push(elem, i, null, null), fullUrl, id, repeats, propName, title + "." + parent); 330 i++; 331 } 332 } 333 334 public void validateDocumentReference(List<ValidationMessage> errors, List<Element> entries, Element composition, NodeStack stack, String fullUrl, String id, boolean repeats, String propName, String title) { 335 if (repeats) { 336 List<Element> list = new ArrayList<>(); 337 composition.getNamedChildren(propName, list); 338 int i = 1; 339 for (Element elem : list) { 340 341 validateBundleReference(errors, entries, elem, title + "." + propName, stack.push(elem, i, null, null), fullUrl, "Composition", id); 342 i++; 343 } 344 345 } else { 346 Element elem = composition.getNamedChild(propName); 347 if (elem != null) { 348 validateBundleReference(errors, entries, elem, title + "." + propName, stack.push(elem, -1, null, null), fullUrl, "Composition", id); 349 } 350 } 351 } 352 353 private void validateMessage(List<ValidationMessage> errors, List<Element> entries, Element messageHeader, NodeStack stack, String fullUrl, String id) { 354 // first entry must be a messageheader 355 if (rule(errors, IssueType.INVALID, messageHeader.line(), messageHeader.col(), stack.getLiteralPath(), messageHeader.getType().equals("MessageHeader"), I18nConstants.VALIDATION_BUNDLE_MESSAGE)) { 356 List<Element> elements = messageHeader.getChildren("focus"); 357 for (Element elem : elements) 358 validateBundleReference(errors, entries, elem, "MessageHeader Data", stack.push(elem, -1, null, null), fullUrl, "MessageHeader", id); 359 } 360 } 361 362 private void validateBundleReference(List<ValidationMessage> errors, List<Element> entries, Element ref, String name, NodeStack stack, String fullUrl, String type, String id) { 363 String reference = null; 364 try { 365 reference = ref.getNamedChildValue("reference"); 366 } catch (Error e) { 367 368 } 369 370 if (ref != null && !Utilities.noString(reference) && !reference.startsWith("#")) { 371 Element target = resolveInBundle(entries, reference, fullUrl, type, id); 372 rule(errors, IssueType.INVALID, ref.line(), ref.col(), stack.addToLiteralPath("reference"), target != null, 373 I18nConstants.BUNDLE_BUNDLE_ENTRY_NOTFOUND, reference, name); 374 } 375 } 376 377 378 /** 379 * As per outline for <a href=http://hl7.org/fhir/stu3/documents.html#content>Document Content</a>: 380 * <li>"The document date (mandatory). This is found in Bundle.meta.lastUpdated and identifies when the document bundle 381 * was assembled from the underlying resources"</li> 382 * <p></p> 383 * This check was not being done for release versions < r4. 384 * <p></p> 385 * Related JIRA ticket is <a href=https://jira.hl7.org/browse/FHIR-26544>FHIR-26544</a> 386 * 387 * @param bundle {@link org.hl7.fhir.r5.elementmodel} 388 * @param errors {@link List<ValidationMessage>} 389 * @param stack {@link NodeStack} 390 */ 391 private void handleSpecialCaseForLastUpdated(Element bundle, List<ValidationMessage> errors, NodeStack stack) { 392 boolean ok = bundle.hasChild(META) 393 && bundle.getNamedChild(META).hasChild(LAST_UPDATED) 394 && bundle.getNamedChild(META).getNamedChild(LAST_UPDATED).hasValue(); 395 ruleHtml(errors, IssueType.REQUIRED, stack.getLiteralPath(), ok, I18nConstants.DOCUMENT_DATE_REQUIRED, I18nConstants.DOCUMENT_DATE_REQUIRED_HTML); 396 } 397 398 private void checkAllInterlinked(List<ValidationMessage> errors, List<Element> entries, NodeStack stack, Element bundle, boolean isError) { 399 List<EntrySummary> entryList = new ArrayList<>(); 400 int i = 0; 401 for (Element entry : entries) { 402 Element r = entry.getNamedChild(RESOURCE); 403 if (r != null) { 404 EntrySummary e = new EntrySummary(i, entry, r); 405 entryList.add(e); 406// System.out.println("Found entry "+e.dbg()); 407 } 408 i++; 409 } 410 411 for (EntrySummary e : entryList) { 412 Set<String> references = findReferences(e.getEntry()); 413 for (String ref : references) { 414 Element tgt = resolveInBundle(entries, ref, e.getEntry().getChildValue(FULL_URL), e.getResource().fhirType(), e.getResource().getIdBase()); 415 if (tgt != null) { 416 EntrySummary t = entryForTarget(entryList, tgt); 417 if (t != null ) { 418 if (t != e) { 419// System.out.println("Entry "+e.getIndex()+" refers to "+t.getIndex()+" by ref '"+ref+"'"); 420 e.getTargets().add(t); 421 } else { 422// System.out.println("Entry "+e.getIndex()+" refers to itself by '"+ref+"'"); 423 } 424 } 425 } 426 } 427 } 428 429 Set<EntrySummary> visited = new HashSet<>(); 430 visitLinked(visited, entryList.get(0)); 431 boolean foundRevLinks; 432 do { 433 foundRevLinks = false; 434 for (EntrySummary e : entryList) { 435 if (!visited.contains(e)) { 436// System.out.println("Not visited "+e.getIndex()+" - check for reverse links"); 437 boolean add = false; 438 for (EntrySummary t : e.getTargets()) { 439 if (visited.contains(t)) { 440 add = true; 441 } 442 } 443 if (add) { 444 warning(errors, IssueType.INFORMATIONAL, e.getEntry().line(), e.getEntry().col(), 445 stack.addToLiteralPath(ENTRY + '[' + (i + 1) + ']'), isExpectedToBeReverse(e.getResource().fhirType()), 446 I18nConstants.BUNDLE_BUNDLE_ENTRY_REVERSE, (e.getEntry().getChildValue(FULL_URL) != null ? "'" + e.getEntry().getChildValue(FULL_URL) + "'" : "")); 447// System.out.println("Found reverse links for "+e.getIndex()); 448 foundRevLinks = true; 449 visitLinked(visited, e); 450 } 451 } 452 } 453 } while (foundRevLinks); 454 455 i = 0; 456 for (EntrySummary e : entryList) { 457 Element entry = e.getEntry(); 458 if (isError) { 459 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) + "'" : "")); 460 } else { 461 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) + "'" : "")); 462 } 463 i++; 464 } 465 } 466 467 468 469 private boolean isExpectedToBeReverse(String fhirType) { 470 return Utilities.existsInList(fhirType, "Provenance"); 471 } 472 473 private String uriRegexForVersion() { 474 if (VersionUtilities.isR3Ver(context.getVersion())) 475 return URI_REGEX3; 476 else 477 return Constants.URI_REGEX; 478 } 479 480 private String getCanonicalURLForEntry(Element entry) { 481 Element e = entry.getNamedChild(RESOURCE); 482 if (e == null) 483 return null; 484 return e.getNamedChildValue("url"); 485 } 486 487 private String getIdForEntry(Element entry) { 488 Element e = entry.getNamedChild(RESOURCE); 489 if (e == null) 490 return null; 491 return e.getNamedChildValue(ID); 492 } 493 494 /** 495 * Check each resource entry to ensure that the entry's fullURL includes the resource's id 496 * value. Adds an ERROR ValidationMessge to errors List for a given entry if it references 497 * a resource and fullURL does not include the resource's id. 498 * 499 * @param errors List of ValidationMessage objects that new errors will be added to. 500 * @param entries List of entry Element objects to be checked. 501 * @param stack Current NodeStack used to create path names in error detail messages. 502 */ 503 private void validateResourceIds(List<ValidationMessage> errors, List<Element> entries, NodeStack stack) { 504 // TODO: Need to handle _version 505 int i = 1; 506 for (Element entry : entries) { 507 String fullUrl = entry.getNamedChildValue(FULL_URL); 508 Element resource = entry.getNamedChild(RESOURCE); 509 String id = resource != null ? resource.getNamedChildValue(ID) : null; 510 if (id != null && fullUrl != null) { 511 String urlId = null; 512 if (fullUrl.startsWith("https://") || fullUrl.startsWith("http://")) { 513 urlId = fullUrl.substring(fullUrl.lastIndexOf('/') + 1); 514 } else if (fullUrl.startsWith("urn:uuid") || fullUrl.startsWith("urn:oid")) { 515 urlId = fullUrl.substring(fullUrl.lastIndexOf(':') + 1); 516 } 517 rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath("entry[" + i + "]"), urlId.equals(id), I18nConstants.BUNDLE_BUNDLE_ENTRY_IDURLMISMATCH, id, fullUrl); 518 } 519 i++; 520 } 521 } 522 523 private EntrySummary entryForTarget(List<EntrySummary> entryList, Element tgt) { 524 for (EntrySummary e : entryList) { 525 if (e.getEntry() == tgt) { 526 return e; 527 } 528 } 529 return null; 530 } 531 532 private void visitLinked(Set<EntrySummary> visited, EntrySummary t) { 533 if (!visited.contains(t)) { 534 visited.add(t); 535 for (EntrySummary e : t.getTargets()) { 536 visitLinked(visited, e); 537 } 538 } 539 } 540 541 private void followResourceLinks(Element entry, Map<String, Element> visitedResources, Map<Element, Element> candidateEntries, List<Element> candidateResources, List<ValidationMessage> errors, NodeStack stack) { 542 followResourceLinks(entry, visitedResources, candidateEntries, candidateResources, errors, stack, 0); 543 } 544 545 private void followResourceLinks(Element entry, Map<String, Element> visitedResources, Map<Element, Element> candidateEntries, List<Element> candidateResources, List<ValidationMessage> errors, NodeStack stack, int depth) { 546 Element resource = entry.getNamedChild(RESOURCE); 547 if (visitedResources.containsValue(resource)) 548 return; 549 550 visitedResources.put(entry.getNamedChildValue(FULL_URL), resource); 551 552 String type = null; 553 Set<String> references = findReferences(resource); 554 for (String reference : references) { 555 // We don't want errors when just retrieving the element as they will be caught (with better path info) in subsequent processing 556 IndexedElement r = getFromBundle(stack.getElement(), reference, entry.getChildValue(FULL_URL), new ArrayList<ValidationMessage>(), stack.addToLiteralPath("entry[" + candidateResources.indexOf(resource) + "]"), type, "transaction".equals(stack.getElement().getChildValue(TYPE))); 557 if (r != null && !visitedResources.containsValue(r.getMatch())) { 558 followResourceLinks(candidateEntries.get(r.getMatch()), visitedResources, candidateEntries, candidateResources, errors, stack, depth + 1); 559 } 560 } 561 } 562 563 564 private Set<String> findReferences(Element start) { 565 Set<String> references = new HashSet<String>(); 566 findReferences(start, references); 567 return references; 568 } 569 570 private void findReferences(Element start, Set<String> references) { 571 for (Element child : start.getChildren()) { 572 if (child.getType().equals("Reference")) { 573 String ref = child.getChildValue("reference"); 574 if (ref != null && !ref.startsWith("#")) 575 references.add(ref); 576 } 577 if (child.getType().equals("url") || child.getType().equals("uri") || child.getType().equals("canonical")) { 578 String ref = child.primitiveValue(); 579 if (ref != null && !ref.startsWith("#")) 580 references.add(ref); 581 } 582 findReferences(child, references); 583 } 584 } 585 586 587 588 // hack for pre-UTG v2/v3 589 private boolean isV3orV2Url(String url) { 590 return url.startsWith("http://hl7.org/fhir/v3/") || url.startsWith("http://hl7.org/fhir/v2/"); 591 } 592 593 594 public boolean meetsRule(BundleValidationRule bvr, String rtype, int rcount, int count) { 595 if (bvr.getRule() == null) { 596 throw new Error(validator.getContext().formatMessage(I18nConstants.BUNDLE_RULE_NONE)); 597 } 598 String rule = bvr.getRule(); 599 String t = rule.contains(":") ? rule.substring(0, rule.indexOf(":")) : Utilities.isInteger(rule) ? null : rule; 600 String index = rule.contains(":") ? rule.substring(rule.indexOf(":")+1) : Utilities.isInteger(rule) ? rule : null; 601 if (Utilities.noString(t) && Utilities.noString(index)) { 602 throw new Error(validator.getContext().formatMessage(I18nConstants.BUNDLE_RULE_NONE)); 603 } 604 if (!Utilities.noString(t)) { 605 if (!validator.getContext().getResourceNames().contains(t)) { 606 throw new Error(validator.getContext().formatMessage(I18nConstants.BUNDLE_RULE_UNKNOWN, t)); 607 } 608 } 609 if (!Utilities.noString(index)) { 610 if (!Utilities.isInteger(index)) { 611 throw new Error(validator.getContext().formatMessage(I18nConstants.BUNDLE_RULE_INVALID_INDEX, index)); 612 } 613 } 614 if (t == null) { 615 return Integer.toString(count).equals(index); 616 } else if (index == null) { 617 return t.equals(rtype); 618 } else { 619 return t.equals(rtype) && Integer.toString(rcount).equals(index); 620 } 621 } 622 623}