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}