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.Base.ValidationMode; 014import org.hl7.fhir.r5.model.Coding; 015import org.hl7.fhir.r5.model.Constants; 016import org.hl7.fhir.r5.model.Enumerations.FHIRVersion; 017import org.hl7.fhir.r5.model.StructureDefinition; 018import org.hl7.fhir.r5.utils.XVerExtensionManager; 019import org.hl7.fhir.r5.utils.validation.BundleValidationRule; 020import org.hl7.fhir.utilities.Utilities; 021import org.hl7.fhir.utilities.VersionUtilities; 022import org.hl7.fhir.utilities.i18n.I18nConstants; 023import org.hl7.fhir.utilities.validation.ValidationMessage; 024import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 025import org.hl7.fhir.validation.BaseValidator; 026import org.hl7.fhir.validation.instance.InstanceValidator; 027import org.hl7.fhir.validation.instance.PercentageTracker; 028import org.hl7.fhir.validation.instance.utils.EntrySummary; 029import org.hl7.fhir.validation.instance.utils.IndexedElement; 030import org.hl7.fhir.validation.instance.utils.NodeStack; 031import org.hl7.fhir.validation.instance.utils.ValidatorHostContext; 032 033public class BundleValidator extends BaseValidator { 034 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})?"; 035 private String serverBase; 036 private InstanceValidator validator; 037 038 public BundleValidator(IWorkerContext context, String serverBase, InstanceValidator validator, XVerExtensionManager xverManager, Coding jurisdiction) { 039 super(context, xverManager); 040 this.serverBase = serverBase; 041 this.validator = validator; 042 this.jurisdiction = jurisdiction; 043 } 044 045 public boolean validateBundle(List<ValidationMessage> errors, Element bundle, NodeStack stack, boolean checkSpecials, ValidatorHostContext hostContext, PercentageTracker pct, ValidationMode mode) { 046 boolean ok = true; 047 String type = bundle.getNamedChildValue(TYPE); 048 type = StringUtils.defaultString(type); 049 List<Element> entries = new ArrayList<Element>(); 050 bundle.getNamedChildren(ENTRY, entries); 051 052 List<Element> links = new ArrayList<Element>(); 053 bundle.getNamedChildren(LINK, links); 054 if (links.size() > 0) { 055 int i = 0; 056 for (Element l : links) { 057 ok = validateLink(errors, bundle, links, l, stack.push(l, i++, null, null), type, entries) && ok; 058 } 059 } 060 061 if (entries.size() == 0) { 062 ok = rule(errors, NO_RULE_DATE, IssueType.INVALID, stack.getLiteralPath(), !(type.equals(DOCUMENT) || type.equals(MESSAGE)), I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRST) && ok; 063 } else { 064 // Get the first entry, the MessageHeader 065 Element firstEntry = entries.get(0); 066 // Get the stack of the first entry 067 NodeStack firstStack = stack.push(firstEntry, 1, null, null); 068 069 String fullUrl = firstEntry.getNamedChildValue(FULL_URL); 070 071 if (type.equals(DOCUMENT)) { 072 Element resource = firstEntry.getNamedChild(RESOURCE); 073 if (rule(errors, NO_RULE_DATE, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), resource != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRSTRESOURCE)) { 074 String id = resource.getNamedChildValue(ID); 075 ok = validateDocument(errors, bundle, entries, resource, firstStack.push(resource, -1, null, null), fullUrl, id) && ok; 076 } 077 if (!VersionUtilities.isThisOrLater(FHIRVersion._4_0_1.getDisplay(), bundle.getProperty().getStructure().getFhirVersion().getDisplay())) { 078 ok = handleSpecialCaseForLastUpdated(bundle, errors, stack) && ok; 079 } 080 ok = checkAllInterlinked(errors, entries, stack, bundle, false) && ok; 081 } 082 if (type.equals(MESSAGE)) { 083 Element resource = firstEntry.getNamedChild(RESOURCE); 084 String id = resource.getNamedChildValue(ID); 085 if (rule(errors, NO_RULE_DATE, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), resource != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRSTRESOURCE)) { 086 ok = validateMessage(errors, entries, resource, firstStack.push(resource, -1, null, null), fullUrl, id) && ok; 087 } 088 ok = checkAllInterlinked(errors, entries, stack, bundle, true) && ok; 089 } 090 if (type.equals(SEARCHSET)) { 091 checkSearchSet(errors, bundle, entries, stack); 092 } 093 // We do not yet have rules requiring that the id and fullUrl match when dealing with messaging Bundles 094 // validateResourceIds(errors, UNKNOWN_DATE_TIME, entries, stack); 095 } 096 097 int count = 0; 098 Map<String, Integer> counter = new HashMap<>(); 099 100 boolean fullUrlOptional = Utilities.existsInList(type, "transaction", "transaction-response", "batch", "batch-response"); 101 102 for (Element entry : entries) { 103 NodeStack estack = stack.push(entry, count, null, null); 104 String fullUrl = entry.getNamedChildValue(FULL_URL); 105 String url = getCanonicalURLForEntry(entry); 106 String id = getIdForEntry(entry); 107 if (url != null) { 108 if (!(!url.equals(fullUrl) || (url.matches(uriRegexForVersion()) && url.endsWith("/" + id))) && !isV3orV2Url(url)) 109 ok = rule(errors, NO_RULE_DATE, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), false, I18nConstants.BUNDLE_BUNDLE_ENTRY_MISMATCHIDURL, url, fullUrl, id) && ok; 110 ok = rule(errors, NO_RULE_DATE, 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) && ok; 111 } 112 113 if (!VersionUtilities.isR2Ver(context.getVersion())) { 114 ok = rule(errors, NO_RULE_DATE, IssueType.INVALID, entry.line(), entry.col(), estack.getLiteralPath(), fullUrlOptional || fullUrl != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_FULLURL_REQUIRED) && ok; 115 } 116 // check bundle profile requests 117 if (entry.hasChild(RESOURCE)) { 118 String rtype = entry.getNamedChild(RESOURCE).fhirType(); 119 int rcount = counter.containsKey(rtype) ? counter.get(rtype)+1 : 0; 120 counter.put(rtype, rcount); 121 for (BundleValidationRule bvr : validator.getBundleValidationRules()) { 122 if (meetsRule(bvr, rtype, rcount, count)) { 123 StructureDefinition defn = validator.getContext().fetchResource(StructureDefinition.class, bvr.getProfile()); 124 if (defn == null) { 125 throw new Error(validator.getContext().formatMessage(I18nConstants.BUNDLE_RULE_PROFILE_UNKNOWN, bvr.getRule(), bvr.getProfile())); 126 } else { 127 Element res = entry.getNamedChild(RESOURCE); 128 NodeStack rstack = estack.push(res, -1, null, null); 129 if (validator.isCrumbTrails()) { 130 res.addMessage(signpost(errors, NO_RULE_DATE, IssueType.INFORMATIONAL, res.line(), res.col(), stack.getLiteralPath(), I18nConstants.VALIDATION_VAL_PROFILE_SIGNPOST_BUNDLE_PARAM, defn.getUrl())); 131 } 132 stack.resetIds(); 133 ok = validator.startInner(hostContext, errors, res, res, defn, rstack, false, pct, mode) && ok; 134 } 135 } 136 } 137 } 138 139 // todo: check specials 140 count++; 141 } 142 return ok; 143 } 144 145 private boolean validateLink(List<ValidationMessage> errors, Element bundle, List<Element> links, Element link, NodeStack stack, String type, List<Element> entries) { 146 switch (type) { 147 case "document": return validateDocumentLink(errors, bundle, links, link, stack, entries); 148 case "message": return validateMessageLink(errors, bundle, links, link, stack, entries); 149 case "history": 150 case "searchset": return validateSearchLink(errors, bundle, links, link, stack); 151 case "collection": return validateCollectionLink(errors, bundle, links, link, stack); 152 case "subscription-notification": return validateSubscriptionLink(errors, bundle, links, link, stack); 153 case "transaction": 154 case "transaction-response": 155 case "batch": 156 case "batch-response": 157 return validateTransactionOrBatchLink(errors, bundle, links, link, stack); 158 default: 159 return true; // unknown document type, deal with that elsewhere 160 } 161// rule(errors, "2022-12-09", IssueType.INVALID, l.line(), l.col(), stack.getLiteralPath(), false, I18nConstants.BUNDLE_LINK_UNKNOWN, ); 162 } 163 164 private boolean validateDocumentLink(List<ValidationMessage> errors, Element bundle, List<Element> links, Element link, NodeStack stack, List<Element> entries) { 165 boolean ok = true; 166 Element relE = link.getNamedChild("relation"); 167 if (relE != null) { 168 NodeStack relStack = stack.push(relE, -1, null, null); 169 String rel = relE.getValue(); 170 ok = rule(errors, "2022-12-09", IssueType.INVALID, relE.line(), relE.col(), relStack.getLiteralPath(), !Utilities.existsInList(rel, "first", "previous", "next", "last"), I18nConstants.BUNDLE_LINK_SEARCH_PROHIBITED, rel); 171 if ("self".equals(rel)) { 172 ok = rule(errors, "2022-12-09", IssueType.INVALID, relE.line(), relE.col(), relStack.getLiteralPath(), relationshipUnique(rel, link, links), I18nConstants.BUNDLE_LINK_SEARCH_NO_DUPLICATES, rel) && ok; 173 } 174 if ("stylesheet".equals(rel)) { 175 Element urlE = link.getNamedChild("url"); 176 if (urlE != null) { 177 NodeStack urlStack = stack.push(urlE, -1, null, null); 178 String url = urlE.getValue(); 179 if (url != null) { 180 if (Utilities.isAbsoluteUrl(url)) { 181 // todo: do we need to consider rel = base? 182 if (url.equals("https://hl7.org/fhir/fhir.css")) { 183 // well, this is ok! 184 } else { 185 warning(errors, "2022-12-09", IssueType.BUSINESSRULE, urlE.line(), urlE.col(), urlStack.getLiteralPath(), false, I18nConstants.BUNDLE_LINK_STYELSHEET_EXTERNAL); 186 if (url.startsWith("http://")) { 187 warning(errors, "2022-12-09", IssueType.BUSINESSRULE, urlE.line(), urlE.col(), urlStack.getLiteralPath(), false, I18nConstants.BUNDLE_LINK_STYELSHEET_INSECURE); 188 } 189 if (!Utilities.isAbsoluteUrlLinkable(url)) { 190 warning(errors, "2022-12-09", IssueType.BUSINESSRULE, urlE.line(), urlE.col(), urlStack.getLiteralPath(), false, I18nConstants.BUNDLE_LINK_STYELSHEET_LINKABLE); 191 } 192 } 193 } else { 194 // has to resolve in the bundle 195 boolean found = false; 196 for (Element e : entries) { 197 Element res = e.getNamedChild("resource"); 198 if (res != null && (""+res.fhirType()+"/"+res.getIdBase()).equals(url)) { 199 found = true; 200 break; 201 } 202 } 203 ok = rule(errors, "2022-12-09", IssueType.NOTFOUND, urlE.line(), urlE.col(), urlStack.getLiteralPath(), found, I18nConstants.BUNDLE_LINK_STYELSHEET_NOT_FOUND) && ok; 204 } 205 } 206 } 207 } 208 } 209 return ok; 210 } 211 212 private boolean validateMessageLink(List<ValidationMessage> errors, Element bundle, List<Element> links, Element link, NodeStack stack, List<Element> entries) { 213 boolean ok = true; 214 Element relE = link.getNamedChild("relation"); 215 if (relE != null) { 216 NodeStack relStack = stack.push(relE, -1, null, null); 217 String rel = relE.getValue(); 218 ok = rule(errors, "2022-12-09", IssueType.INVALID, relE.line(), relE.col(), relStack.getLiteralPath(), !Utilities.existsInList(rel, "first", "previous", "next", "last"), I18nConstants.BUNDLE_LINK_SEARCH_PROHIBITED, rel); 219 if ("self".equals(rel)) { 220 ok = rule(errors, "2022-12-09", IssueType.INVALID, relE.line(), relE.col(), relStack.getLiteralPath(), relationshipUnique(rel, link, links), I18nConstants.BUNDLE_LINK_SEARCH_NO_DUPLICATES, rel) && ok; 221 } 222 } 223 return ok; 224 } 225 226 private boolean validateSearchLink(List<ValidationMessage> errors, Element bundle, List<Element> links, Element link, NodeStack stack) { 227 String rel = StringUtils.defaultString(link.getNamedChildValue("relation")); 228 if (Utilities.existsInList(rel, "first", "previous", "next", "last", "self")) { 229 return rule(errors, "2022-12-09", IssueType.INVALID, link.line(), link.col(), stack.getLiteralPath(), relationshipUnique(rel, link, links), I18nConstants.BUNDLE_LINK_SEARCH_NO_DUPLICATES, rel); 230 } else { 231 return true; 232 } 233 } 234 235 private boolean relationshipUnique(String rel, Element link, List<Element> links) { 236 for (Element l : links) { 237 if (l != link && rel.equals(l.getNamedChildValue("relation"))) { 238 return false; 239 } 240 if (l == link) { 241 // we only want to complain once, so we only look above this one 242 return true; 243 } 244 } 245 return true; 246 } 247 248 private boolean validateCollectionLink(List<ValidationMessage> errors, Element bundle, List<Element> links, Element link, NodeStack stack) { 249 boolean ok = true; 250 Element relE = link.getNamedChild("relation"); 251 if (relE != null) { 252 NodeStack relStack = stack.push(relE, -1, null, null); 253 String rel = relE.getValue(); 254 ok = rule(errors, "2022-12-09", IssueType.INVALID, relE.line(), relE.col(), relStack.getLiteralPath(), !Utilities.existsInList(rel, "first", "previous", "next", "last"), I18nConstants.BUNDLE_LINK_SEARCH_PROHIBITED, rel); 255 if ("self".equals(rel)) { 256 ok = rule(errors, "2022-12-09", IssueType.INVALID, relE.line(), relE.col(), relStack.getLiteralPath(), relationshipUnique(rel, link, links), I18nConstants.BUNDLE_LINK_SEARCH_NO_DUPLICATES, rel) && ok; 257 } 258 } 259 return ok; 260 } 261 262 private boolean validateSubscriptionLink(List<ValidationMessage> errors, Element bundle, List<Element> links, Element link, NodeStack stack) { 263 boolean ok = true; 264 Element relE = link.getNamedChild("relation"); 265 if (relE != null) { 266 NodeStack relStack = stack.push(relE, -1, null, null); 267 String rel = relE.getValue(); 268 ok = rule(errors, "2022-12-09", IssueType.INVALID, relE.line(), relE.col(), relStack.getLiteralPath(), !Utilities.existsInList(rel, "first", "previous", "next", "last"), I18nConstants.BUNDLE_LINK_SEARCH_PROHIBITED, rel); 269 if ("self".equals(rel)) { 270 ok = rule(errors, "2022-12-09", IssueType.INVALID, relE.line(), relE.col(), relStack.getLiteralPath(), relationshipUnique(rel, link, links), I18nConstants.BUNDLE_LINK_SEARCH_NO_DUPLICATES, rel) && ok; 271 } 272 } 273 return ok; 274 } 275 276 private boolean validateTransactionOrBatchLink(List<ValidationMessage> errors, Element bundle, List<Element> links, Element link, NodeStack stack) { 277 boolean ok = true; 278 Element relE = link.getNamedChild("relation"); 279 if (relE != null) { 280 NodeStack relStack = stack.push(relE, -1, null, null); 281 String rel = relE.getValue(); 282 ok = rule(errors, "2022-12-09", IssueType.INVALID, relE.line(), relE.col(), relStack.getLiteralPath(), !Utilities.existsInList(rel, "first", "previous", "next", "last"), I18nConstants.BUNDLE_LINK_SEARCH_PROHIBITED, rel); 283 if ("self".equals(rel)) { 284 ok = rule(errors, "2022-12-09", IssueType.INVALID, relE.line(), relE.col(), relStack.getLiteralPath(), relationshipUnique(rel, link, links), I18nConstants.BUNDLE_LINK_SEARCH_NO_DUPLICATES, rel) && ok; 285 } 286 } 287 return ok; 288 } 289 290 private void checkSearchSet(List<ValidationMessage> errors, Element bundle, List<Element> entries, NodeStack stack) { 291 // warning: should have self link 292 List<Element> links = new ArrayList<Element>(); 293 bundle.getNamedChildren(LINK, links); 294 Element selfLink = getSelfLink(links); 295 List<String> types = new ArrayList<>(); 296 if (selfLink == null) { 297 warning(errors, NO_RULE_DATE, IssueType.INVALID, bundle.line(), bundle.col(), stack.getLiteralPath(), false, I18nConstants.BUNDLE_SEARCH_NOSELF); 298 } else { 299 readSearchResourceTypes(selfLink.getNamedChildValue("url"), types); 300 if (types.size() == 0) { 301 hint(errors, NO_RULE_DATE, IssueType.INVALID, bundle.line(), bundle.col(), stack.getLiteralPath(), false, I18nConstants.BUNDLE_SEARCH_SELF_NOT_UNDERSTOOD); 302 } 303 } 304 305 Boolean searchMode = readHasSearchMode(entries); 306 if (searchMode != null && searchMode == false) { // if no resources have search mode 307 boolean typeProblem = false; 308 String rtype = null; 309 int count = 0; 310 for (Element entry : entries) { 311 NodeStack estack = stack.push(entry, count, null, null); 312 count++; 313 Element res = entry.getNamedChild("resource"); 314 if (rule(errors, NO_RULE_DATE, IssueType.INVALID, bundle.line(), bundle.col(), estack.getLiteralPath(), res != null, I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE)) { 315 NodeStack rstack = estack.push(res, -1, null, null); 316 String rt = res.fhirType(); 317 Boolean ok = checkSearchType(types, rt); 318 if (ok == null) { 319 typeProblem = true; 320 hint(errors, NO_RULE_DATE, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), selfLink == null, I18nConstants.BUNDLE_SEARCH_ENTRY_TYPE_NOT_SURE); 321 String id = res.getNamedChildValue("id"); 322 warning(errors, NO_RULE_DATE, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), id != null || "OperationOutcome".equals(rt), I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE_ID); 323 } else if (ok) { 324 if (!"OperationOutcome".equals(rt)) { 325 String id = res.getNamedChildValue("id"); 326 warning(errors, NO_RULE_DATE, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), id != null, I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE_ID); 327 if (rtype != null && !rt.equals(rtype)) { 328 typeProblem = true; 329 } else if (rtype == null) { 330 rtype = rt; 331 } 332 } 333 } else { 334 typeProblem = true; 335 warning(errors, NO_RULE_DATE, IssueType.INVALID, bundle.line(), bundle.col(), estack.getLiteralPath(), false, I18nConstants.BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_NO_MODE, rt, types); 336 } 337 } 338 } 339 if (typeProblem) { 340 warning(errors, NO_RULE_DATE, IssueType.INVALID, bundle.line(), bundle.col(), stack.getLiteralPath(), !typeProblem, I18nConstants.BUNDLE_SEARCH_NO_MODE); 341 } else { 342 hint(errors, NO_RULE_DATE, IssueType.INVALID, bundle.line(), bundle.col(), stack.getLiteralPath(), !typeProblem, I18nConstants.BUNDLE_SEARCH_NO_MODE); 343 } 344 } else { 345 int count = 0; 346 for (Element entry : entries) { 347 NodeStack estack = stack.push(entry, count, null, null); 348 count++; 349 Element res = entry.getNamedChild("resource"); 350 String sm = null; 351 Element s = entry.getNamedChild("search"); 352 if (s != null) { 353 sm = s.getNamedChildValue("mode"); 354 } 355 warning(errors, NO_RULE_DATE, IssueType.INVALID, bundle.line(), bundle.col(), estack.getLiteralPath(), sm != null, I18nConstants.BUNDLE_SEARCH_NO_MODE); 356 if (rule(errors, NO_RULE_DATE, IssueType.INVALID, bundle.line(), bundle.col(), estack.getLiteralPath(), res != null, I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE)) { 357 NodeStack rstack = estack.push(res, -1, null, null); 358 String rt = res.fhirType(); 359 String id = res.getNamedChildValue("id"); 360 if (sm != null) { 361 if ("match".equals(sm)) { 362 rule(errors, NO_RULE_DATE, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), id != null, I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE_ID); 363 rule(errors, NO_RULE_DATE, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), types.size() == 0 || checkSearchType(types, rt), I18nConstants.BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_MODE, rt, types); 364 } else if ("include".equals(sm)) { 365 rule(errors, NO_RULE_DATE, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), id != null, I18nConstants.BUNDLE_SEARCH_ENTRY_NO_RESOURCE_ID); 366 } else { // outcome 367 rule(errors, NO_RULE_DATE, IssueType.INVALID, bundle.line(), bundle.col(), rstack.getLiteralPath(), "OperationOutcome".equals(rt), I18nConstants.BUNDLE_SEARCH_ENTRY_WRONG_RESOURCE_TYPE_OUTCOME, rt); 368 } 369 } 370 } 371 } 372 } 373 } 374 375 private Boolean checkSearchType(List<String> types, String rt) { 376 if (types.size() == 0) { 377 return null; 378 } else { 379 return Utilities.existsInList(rt, types); 380 } 381 } 382 383 private Boolean readHasSearchMode(List<Element> entries) { 384 boolean all = true; 385 boolean any = false; 386 for (Element entry : entries) { 387 String sm = null; 388 Element s = entry.getNamedChild("search"); 389 if (s != null) { 390 sm = s.getNamedChildValue("mode"); 391 } 392 if (sm != null) { 393 any = true; 394 } else { 395 all = false; 396 } 397 } 398 if (all) { 399 return true; 400 } else if (any) { 401 return null; 402 } else { 403 return false; 404 } 405 } 406 407 private void readSearchResourceTypes(String ref, List<String> types) { 408 if (ref == null) { 409 return; 410 } 411 String[] head = null; 412 String[] tail = null; 413 if (ref.contains("?")) { 414 head = ref.substring(0, ref.indexOf("?")).split("\\/"); 415 tail = ref.substring(ref.indexOf("?")+1).split("\\&"); 416 } else { 417 head = ref.split("\\/"); 418 } 419 if (head == null || head.length == 0) { 420 return; 421 } else if (context.getResourceNames().contains(head[head.length-1])) { 422 types.add(head[head.length-1]); 423 } else if (tail != null) { 424 for (String s : tail) { 425 if (s.startsWith("_type=")) { 426 for (String t : s.substring(6).split("\\,")) { 427 types.add(t); 428 } 429 } 430 } 431 } 432 } 433 434 private Element getSelfLink(List<Element> links) { 435 for (Element link : links) { 436 if ("self".equals(link.getNamedChildValue("relation"))) { 437 return link; 438 } 439 } 440 return null; 441 } 442 443 private boolean validateDocument(List<ValidationMessage> errors, Element bundle, List<Element> entries, Element composition, NodeStack stack, String fullUrl, String id) { 444 boolean ok = true; 445 // first entry must be a composition 446 if (rule(errors, NO_RULE_DATE, IssueType.INVALID, composition.line(), composition.col(), stack.getLiteralPath(), composition.getType().equals("Composition"), I18nConstants.BUNDLE_BUNDLE_ENTRY_DOCUMENT)) { 447 448 // the composition subject etc references must resolve in the bundle 449 ok = validateDocumentReference(errors, bundle, entries, composition, stack, fullUrl, id, false, "subject", "Composition") && ok; 450 ok = validateDocumentReference(errors, bundle, entries, composition, stack, fullUrl, id, true, "author", "Composition") && ok; 451 ok = validateDocumentReference(errors, bundle, entries, composition, stack, fullUrl, id, false, "encounter", "Composition") && ok; 452 ok = validateDocumentReference(errors, bundle, entries, composition, stack, fullUrl, id, false, "custodian", "Composition") && ok; 453 ok = validateDocumentSubReference(errors, bundle, entries, composition, stack, fullUrl, id, "Composition", "attester", false, "party") && ok; 454 ok = validateDocumentSubReference(errors, bundle, entries, composition, stack, fullUrl, id, "Composition", "event", true, "detail") && ok; 455 456 ok = validateSections(errors, bundle, entries, composition, stack, fullUrl, id) && ok; 457 } else { 458 ok = false; 459 } 460 return ok; 461 } 462 463 private boolean validateSections(List<ValidationMessage> errors, Element bundle, List<Element> entries, Element focus, NodeStack stack, String fullUrl, String id) { 464 boolean ok = true; 465 List<Element> sections = new ArrayList<Element>(); 466 focus.getNamedChildren("section", sections); 467 int i = 1; 468 for (Element section : sections) { 469 NodeStack localStack = stack.push(section, i, null, null); 470 471 // technically R4+, but there won't be matches from before that 472 ok = validateDocumentReference(errors, bundle, entries, section, stack, fullUrl, id, true, "author", "Section") && ok; 473 ok = validateDocumentReference(errors, bundle, entries, section, stack, fullUrl, id, false, "focus", "Section") && ok; 474 475 List<Element> sectionEntries = new ArrayList<Element>(); 476 section.getNamedChildren(ENTRY, sectionEntries); 477 int j = 1; 478 for (Element sectionEntry : sectionEntries) { 479 NodeStack localStack2 = localStack.push(sectionEntry, j, null, null); 480 ok = validateBundleReference(errors, bundle, entries, sectionEntry, "Section Entry", localStack2, fullUrl, "Composition", id) && ok; 481 j++; 482 } 483 ok = validateSections(errors, bundle, entries, section, localStack, fullUrl, id) && ok; 484 i++; 485 } 486 return ok; 487 } 488 489 490 public boolean validateDocumentSubReference(List<ValidationMessage> errors, Element bundle, List<Element> entries, Element composition, NodeStack stack, String fullUrl, String id, String title, String parent, boolean repeats, String propName) { 491 boolean ok = true; 492 List<Element> list = new ArrayList<>(); 493 composition.getNamedChildren(parent, list); 494 int i = 1; 495 for (Element elem : list) { 496 ok = validateDocumentReference(errors, bundle, entries, elem, stack.push(elem, i, null, null), fullUrl, id, repeats, propName, title + "." + parent) && ok; 497 i++; 498 } 499 return ok; 500 } 501 502 public boolean validateDocumentReference(List<ValidationMessage> errors, Element bundle, List<Element> entries, Element composition, NodeStack stack, String fullUrl, String id, boolean repeats, String propName, String title) { 503 boolean ok = true; 504 if (repeats) { 505 List<Element> list = new ArrayList<>(); 506 composition.getNamedChildren(propName, list); 507 int i = 1; 508 for (Element elem : list) { 509 ok = validateBundleReference(errors, bundle, entries, elem, title + "." + propName, stack.push(elem, i, null, null), fullUrl, "Composition", id) && ok; 510 i++; 511 } 512 513 } else { 514 Element elem = composition.getNamedChild(propName); 515 if (elem != null) { 516 ok = validateBundleReference(errors, bundle, entries, elem, title + "." + propName, stack.push(elem, -1, null, null), fullUrl, "Composition", id) && ok; 517 } 518 } 519 return ok; 520 } 521 522 private boolean validateMessage(List<ValidationMessage> errors, List<Element> entries, Element messageHeader, NodeStack stack, String fullUrl, String id) { 523 boolean ok = true; 524 // first entry must be a messageheader 525 if (rule(errors, NO_RULE_DATE, IssueType.INVALID, messageHeader.line(), messageHeader.col(), stack.getLiteralPath(), messageHeader.getType().equals("MessageHeader"), I18nConstants.VALIDATION_BUNDLE_MESSAGE)) { 526 List<Element> elements = messageHeader.getChildren("focus"); 527 for (Element elem : elements) 528 ok = validateBundleReference(errors, messageHeader, entries, elem, "MessageHeader Data", stack.push(elem, -1, null, null), fullUrl, "MessageHeader", id) && ok; 529 } 530 return ok; 531 } 532 533 private boolean validateBundleReference(List<ValidationMessage> errors, Element bundle, List<Element> entries, Element ref, String name, NodeStack stack, String fullUrl, String type, String id) { 534 String reference = null; 535 try { 536 reference = ref.getNamedChildValue("reference"); 537 } catch (Error e) { 538 } 539 540 if (ref != null && !Utilities.noString(reference) && !reference.startsWith("#")) { 541 Element target = resolveInBundle(bundle, entries, reference, fullUrl, type, id); 542 return rule(errors, NO_RULE_DATE, IssueType.INVALID, ref.line(), ref.col(), stack.addToLiteralPath("reference"), target != null, 543 I18nConstants.BUNDLE_BUNDLE_ENTRY_NOTFOUND, reference, name); 544 } 545 return true; 546 } 547 548 549 /** 550 * As per outline for <a href=http://hl7.org/fhir/stu3/documents.html#content>Document Content</a>: 551 * <li>"The document date (mandatory). This is found in Bundle.meta.lastUpdated and identifies when the document bundle 552 * was assembled from the underlying resources"</li> 553 * <p></p> 554 * This check was not being done for release versions < r4. 555 * <p></p> 556 * Related JIRA ticket is <a href=https://jira.hl7.org/browse/FHIR-26544>FHIR-26544</a> 557 * 558 * @param bundle {@link org.hl7.fhir.r5.elementmodel} 559 * @param errors {@link List<ValidationMessage>} 560 * @param stack {@link NodeStack} 561 */ 562 private boolean handleSpecialCaseForLastUpdated(Element bundle, List<ValidationMessage> errors, NodeStack stack) { 563 boolean ok = bundle.hasChild(META) 564 && bundle.getNamedChild(META).hasChild(LAST_UPDATED) 565 && bundle.getNamedChild(META).getNamedChild(LAST_UPDATED).hasValue(); 566 ruleHtml(errors, NO_RULE_DATE, IssueType.REQUIRED, stack.getLiteralPath(), ok, I18nConstants.DOCUMENT_DATE_REQUIRED, I18nConstants.DOCUMENT_DATE_REQUIRED_HTML); 567 return ok; 568 } 569 570 private boolean checkAllInterlinked(List<ValidationMessage> errors, List<Element> entries, NodeStack stack, Element bundle, boolean isMessage) { 571 boolean ok = true; 572 List<EntrySummary> entryList = new ArrayList<>(); 573 int i = 0; 574 for (Element entry : entries) { 575 Element r = entry.getNamedChild(RESOURCE); 576 if (r != null) { 577 EntrySummary e = new EntrySummary(i, entry, r); 578 entryList.add(e); 579// System.out.println("Found entry "+e.dbg()); 580 } 581 i++; 582 } 583 584 for (EntrySummary e : entryList) { 585 Set<String> references = findReferences(e.getEntry()); 586 for (String ref : references) { 587 Element tgt = resolveInBundle(bundle, entries, ref, e.getEntry().getChildValue(FULL_URL), e.getResource().fhirType(), e.getResource().getIdBase()); 588 if (tgt != null) { 589 EntrySummary t = entryForTarget(entryList, tgt); 590 if (t != null ) { 591 if (t != e) { 592// System.out.println("Entry "+e.getIndex()+" refers to "+t.getIndex()+" by ref '"+ref+"'"); 593 e.getTargets().add(t); 594 } else { 595// System.out.println("Entry "+e.getIndex()+" refers to itself by '"+ref+"'"); 596 } 597 } 598 } 599 } 600 } 601 602 Set<EntrySummary> visited = new HashSet<>(); 603 visitLinked(visited, entryList.get(0)); 604 visitBundleLinks(visited, entryList, bundle); 605 boolean foundRevLinks; 606 do { 607 foundRevLinks = false; 608 for (EntrySummary e : entryList) { 609 if (!visited.contains(e)) { 610// System.out.println("Not visited "+e.getIndex()+" - check for reverse links"); 611 boolean add = false; 612 for (EntrySummary t : e.getTargets()) { 613 if (visited.contains(t)) { 614 add = true; 615 } 616 } 617 if (add) { 618 if (isMessage) { 619 hint(errors, NO_RULE_DATE, IssueType.INFORMATIONAL, e.getEntry().line(), e.getEntry().col(), 620 stack.addToLiteralPath(ENTRY + '[' + (i + 1) + ']'), isExpectedToBeReverse(e.getResource().fhirType()), 621 I18nConstants.BUNDLE_BUNDLE_ENTRY_REVERSE_MSG, (e.getEntry().getChildValue(FULL_URL) != null ? "'" + e.getEntry().getChildValue(FULL_URL) + "'" : "")); 622 } else { 623 // this was illegal up to R4B, but changed to be legal in R5 624 if (VersionUtilities.isR5VerOrLater(context.getVersion())) { 625 hint(errors, NO_RULE_DATE, IssueType.INFORMATIONAL, e.getEntry().line(), e.getEntry().col(), 626 stack.addToLiteralPath(ENTRY + '[' + (i + 1) + ']'), isExpectedToBeReverse(e.getResource().fhirType()), 627 I18nConstants.BUNDLE_BUNDLE_ENTRY_REVERSE_R4, (e.getEntry().getChildValue(FULL_URL) != null ? "'" + e.getEntry().getChildValue(FULL_URL) + "'" : "")); 628 } else { 629 warning(errors, NO_RULE_DATE, IssueType.INVALID, e.getEntry().line(), e.getEntry().col(), 630 stack.addToLiteralPath(ENTRY + '[' + (i + 1) + ']'), isExpectedToBeReverse(e.getResource().fhirType()), 631 I18nConstants.BUNDLE_BUNDLE_ENTRY_REVERSE_R4, (e.getEntry().getChildValue(FULL_URL) != null ? "'" + e.getEntry().getChildValue(FULL_URL) + "'" : "")); 632 } 633 } 634 foundRevLinks = true; 635 visitLinked(visited, e); 636 } 637 } 638 } 639 } while (foundRevLinks); 640 641 i = 0; 642 for (EntrySummary e : entryList) { 643 Element entry = e.getEntry(); 644 if (isMessage) { 645 warning(errors, NO_RULE_DATE, IssueType.INFORMATIONAL, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY + '[' + (i + 1) + ']'), visited.contains(e), I18nConstants.BUNDLE_BUNDLE_ENTRY_ORPHAN_MESSAGE, (entry.getChildValue(FULL_URL) != null ? "'" + entry.getChildValue(FULL_URL) + "'" : "")); 646 } else { 647 ok = rule(errors, NO_RULE_DATE, IssueType.INFORMATIONAL, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY + '[' + (i + 1) + ']'), visited.contains(e), I18nConstants.BUNDLE_BUNDLE_ENTRY_ORPHAN_DOCUMENT, (entry.getChildValue(FULL_URL) != null ? "'" + entry.getChildValue(FULL_URL) + "'" : "")) && ok; 648 } 649 i++; 650 } 651 return ok; 652 } 653 654 655 656 private void visitBundleLinks(Set<EntrySummary> visited, List<EntrySummary> entryList, Element bundle) { 657 List<Element> links = bundle.getChildrenByName("link"); 658 for (Element link : links) { 659 String rel = link.getNamedChildValue("relation"); 660 String url = link.getNamedChildValue("url"); 661 if (rel != null && url != null) { 662 if (Utilities.existsInList(rel, "stylesheet")) { 663 for (EntrySummary e : entryList) { 664 if (e.getResource() != null) { 665 if (url.equals(e.getResource().fhirType()+"/"+e.getResource().getIdBase())) { 666 visited.add(e); 667 break; 668 } 669 } 670 } 671 } 672 } 673 } 674 } 675 676 private boolean isExpectedToBeReverse(String fhirType) { 677 return Utilities.existsInList(fhirType, "Provenance"); 678 } 679 680 private String uriRegexForVersion() { 681 if (VersionUtilities.isR3Ver(context.getVersion())) 682 return URI_REGEX3; 683 else 684 return Constants.URI_REGEX; 685 } 686 687 private String getCanonicalURLForEntry(Element entry) { 688 Element e = entry.getNamedChild(RESOURCE); 689 if (e == null) 690 return null; 691 return e.getNamedChildValue("url"); 692 } 693 694 private String getIdForEntry(Element entry) { 695 Element e = entry.getNamedChild(RESOURCE); 696 if (e == null) 697 return null; 698 return e.getNamedChildValue(ID); 699 } 700 701 /** 702 * Check each resource entry to ensure that the entry's fullURL includes the resource's id 703 * value. Adds an ERROR ValidationMessge to errors List for a given entry if it references 704 * a resource and fullURL does not include the resource's id. 705 * 706 * @param errors List of ValidationMessage objects that new errors will be added to. 707 * @param entries List of entry Element objects to be checked. 708 * @param stack Current NodeStack used to create path names in error detail messages. 709 */ 710 private void validateResourceIds(List<ValidationMessage> errors, List<Element> entries, NodeStack stack) { 711 // TODO: Need to handle _version 712 int i = 1; 713 for (Element entry : entries) { 714 String fullUrl = entry.getNamedChildValue(FULL_URL); 715 Element resource = entry.getNamedChild(RESOURCE); 716 String id = resource != null ? resource.getNamedChildValue(ID) : null; 717 if (id != null && fullUrl != null) { 718 String urlId = null; 719 if (fullUrl.startsWith("https://") || fullUrl.startsWith("http://")) { 720 urlId = fullUrl.substring(fullUrl.lastIndexOf('/') + 1); 721 } else if (fullUrl.startsWith("urn:uuid") || fullUrl.startsWith("urn:oid")) { 722 urlId = fullUrl.substring(fullUrl.lastIndexOf(':') + 1); 723 } 724 rule(errors, NO_RULE_DATE, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath("entry[" + i + "]"), urlId.equals(id), I18nConstants.BUNDLE_BUNDLE_ENTRY_IDURLMISMATCH, id, fullUrl); 725 } 726 i++; 727 } 728 } 729 730 private EntrySummary entryForTarget(List<EntrySummary> entryList, Element tgt) { 731 for (EntrySummary e : entryList) { 732 if (e.getEntry() == tgt) { 733 return e; 734 } 735 } 736 return null; 737 } 738 739 private void visitLinked(Set<EntrySummary> visited, EntrySummary t) { 740 if (!visited.contains(t)) { 741 visited.add(t); 742 for (EntrySummary e : t.getTargets()) { 743 visitLinked(visited, e); 744 } 745 } 746 } 747 748 private void followResourceLinks(Element entry, Map<String, Element> visitedResources, Map<Element, Element> candidateEntries, List<Element> candidateResources, List<ValidationMessage> errors, NodeStack stack) { 749 followResourceLinks(entry, visitedResources, candidateEntries, candidateResources, errors, stack, 0); 750 } 751 752 private void followResourceLinks(Element entry, Map<String, Element> visitedResources, Map<Element, Element> candidateEntries, List<Element> candidateResources, List<ValidationMessage> errors, NodeStack stack, int depth) { 753 Element resource = entry.getNamedChild(RESOURCE); 754 if (visitedResources.containsValue(resource)) 755 return; 756 757 visitedResources.put(entry.getNamedChildValue(FULL_URL), resource); 758 759 String type = null; 760 Set<String> references = findReferences(resource); 761 for (String reference : references) { 762 // We don't want errors when just retrieving the element as they will be caught (with better path info) in subsequent processing 763 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))); 764 if (r != null && !visitedResources.containsValue(r.getMatch())) { 765 followResourceLinks(candidateEntries.get(r.getMatch()), visitedResources, candidateEntries, candidateResources, errors, stack, depth + 1); 766 } 767 } 768 } 769 770 771 private Set<String> findReferences(Element start) { 772 Set<String> references = new HashSet<String>(); 773 findReferences(start, references); 774 return references; 775 } 776 777 private void findReferences(Element start, Set<String> references) { 778 for (Element child : start.getChildren()) { 779 if (child.getType().equals("Reference")) { 780 String ref = child.getChildValue("reference"); 781 if (ref != null && !ref.startsWith("#")) 782 references.add(ref); 783 } 784 if (child.getType().equals("url") || child.getType().equals("uri") || child.getType().equals("canonical")) { 785 String ref = child.primitiveValue(); 786 if (ref != null && !ref.startsWith("#")) 787 references.add(ref); 788 } 789 findReferences(child, references); 790 } 791 } 792 793 794 795 // hack for pre-UTG v2/v3 796 private boolean isV3orV2Url(String url) { 797 return url.startsWith("http://hl7.org/fhir/v3/") || url.startsWith("http://hl7.org/fhir/v2/"); 798 } 799 800 801 public boolean meetsRule(BundleValidationRule bvr, String rtype, int rcount, int count) { 802 if (bvr.getRule() == null) { 803 throw new Error(validator.getContext().formatMessage(I18nConstants.BUNDLE_RULE_NONE)); 804 } 805 String rule = bvr.getRule(); 806 String t = rule.contains(":") ? rule.substring(0, rule.indexOf(":")) : Utilities.isInteger(rule) ? null : rule; 807 String index = rule.contains(":") ? rule.substring(rule.indexOf(":")+1) : Utilities.isInteger(rule) ? rule : null; 808 if (Utilities.noString(t) && Utilities.noString(index)) { 809 throw new Error(validator.getContext().formatMessage(I18nConstants.BUNDLE_RULE_NONE)); 810 } 811 if (!Utilities.noString(t)) { 812 if (!validator.getContext().getResourceNames().contains(t)) { 813 throw new Error(validator.getContext().formatMessage(I18nConstants.BUNDLE_RULE_UNKNOWN, t)); 814 } 815 } 816 if (!Utilities.noString(index)) { 817 if (!Utilities.isInteger(index)) { 818 throw new Error(validator.getContext().formatMessage(I18nConstants.BUNDLE_RULE_INVALID_INDEX, index)); 819 } 820 } 821 if (t == null) { 822 return Integer.toString(count).equals(index); 823 } else if (index == null) { 824 return t.equals(rtype); 825 } else { 826 return t.equals(rtype) && Integer.toString(rcount).equals(index); 827 } 828 } 829 830}