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}