001package org.hl7.fhir.r5.renderers;
002
003import java.io.IOException;
004import java.io.UnsupportedEncodingException;
005import java.util.List;
006
007import org.hl7.fhir.exceptions.DefinitionException;
008import org.hl7.fhir.exceptions.FHIRException;
009import org.hl7.fhir.exceptions.FHIRFormatError;
010import org.hl7.fhir.r5.model.Base;
011import org.hl7.fhir.r5.model.Bundle;
012import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent;
013import org.hl7.fhir.r5.model.Bundle.BundleEntryRequestComponent;
014import org.hl7.fhir.r5.model.Bundle.BundleEntryResponseComponent;
015import org.hl7.fhir.r5.model.Bundle.BundleEntrySearchComponent;
016import org.hl7.fhir.r5.model.Bundle.BundleType;
017import org.hl7.fhir.r5.model.Composition;
018import org.hl7.fhir.r5.model.Composition.SectionComponent;
019import org.hl7.fhir.r5.model.DomainResource;
020import org.hl7.fhir.r5.model.Property;
021import org.hl7.fhir.r5.model.Provenance;
022import org.hl7.fhir.r5.model.Reference;
023import org.hl7.fhir.r5.model.Resource;
024import org.hl7.fhir.r5.renderers.utils.BaseWrappers.BaseWrapper;
025import org.hl7.fhir.r5.renderers.utils.BaseWrappers.ResourceWrapper;
026import org.hl7.fhir.r5.renderers.utils.RenderingContext;
027import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceContext;
028import org.hl7.fhir.r5.utils.EOperationOutcome;
029import org.hl7.fhir.utilities.xhtml.NodeType;
030import org.hl7.fhir.utilities.xhtml.XhtmlNode;
031
032public class BundleRenderer extends ResourceRenderer {
033
034  
035  public BundleRenderer(RenderingContext context, ResourceContext rcontext) {
036    super(context, rcontext);
037  }
038
039  public BundleRenderer(RenderingContext context) {
040    super(context);
041  }
042
043
044  public BundleRenderer setMultiLangMode(boolean multiLangMode) {
045    this.multiLangMode = multiLangMode;
046    return this;
047  }
048  
049  @Override
050  public boolean render(XhtmlNode x, Resource r) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome {
051    XhtmlNode n = render((Bundle) r);
052    x.addChildren(n.getChildNodes());
053    return false;
054  }
055
056  @Override
057  public String display(Resource r) throws UnsupportedEncodingException, IOException {
058    return null;
059  }
060
061  @Override
062  public String display(ResourceWrapper r) throws UnsupportedEncodingException, IOException {
063    return null;
064  }
065
066  @Override
067  public boolean render(XhtmlNode x, ResourceWrapper b) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome {
068    List<BaseWrapper> entries = b.children("entry");
069    if ("document".equals(b.get("type").primitiveValue())) {
070      if (entries.isEmpty() || (entries.get(0).has("resource") && !"Composition".equals(entries.get(0).get("resource").fhirType())))
071        throw new FHIRException(context.formatPhrase(RenderingContext.BUND_REND_INVALID_DOC, b.getId(), entries.get(0).get("resource").fhirType()+"')"));
072      return renderDocument(x, b, entries);
073    } else if ("collection".equals(b.get("type").primitiveValue()) && allEntriesAreHistoryProvenance(entries)) {
074      // nothing
075    } else {
076      XhtmlNode root = new XhtmlNode(NodeType.Element, "div");
077      root.para().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_ROOT, b.getId(), b.get("type").primitiveValue()));
078      int i = 0;
079      for (BaseWrapper be : entries) {
080        i++;
081        if (be.has("fullUrl")) {
082          root.an(makeInternalBundleLink(be.get("fullUrl").primitiveValue()));
083        }
084        if (be.has("resource")) {
085          if (be.getChildByName("resource").getValues().get(0).has("id")) {
086            root.an(be.get("resource").fhirType() + "_" + be.getChildByName("resource").getValues().get(0).get("id").primitiveValue());
087            root.an("hc"+be.get("resource").fhirType() + "_" + be.getChildByName("resource").getValues().get(0).get("id").primitiveValue());
088          } else {
089            String id = makeIdFromBundleEntry(be.get("fullUrl").primitiveValue());
090            root.an(be.get("resource").fhirType() + "_" + id);
091            root.an("hc"+be.get("resource").fhirType() + "_" + id);
092          }
093        }
094        root.hr();
095        if (be.has("fullUrl")) {
096          root.para().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_ENTRY_URL, Integer.toString(i), be.get("fullUrl").primitiveValue()));
097        } else {
098          root.para().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_ENTRY, Integer.toString(i)));
099        }
100//        if (be.hasRequest())
101//          renderRequest(root, be.getRequest());
102//        if (be.hasSearch())
103//          renderSearch(root, be.getSearch());
104//        if (be.hasResponse())
105//          renderResponse(root, be.getResponse());
106        if (be.has("resource")) {
107          root.para().addText(formatPhrase(RenderingContext.BUNDLE_RESOURCE, be.get("resource").fhirType()));
108          ResourceWrapper rw = be.getChildByName("resource").getAsResource();
109          XhtmlNode xn = rw.getNarrative();
110          if (xn == null || xn.isEmpty()) {
111            ResourceRenderer rr = RendererFactory.factory(rw, context);
112            try {
113              rr.setRcontext(new ResourceContext(rcontext, rw));
114              xn = rr.render(rw);
115            } catch (Exception e) {
116              xn = new XhtmlNode();
117              xn.para().b().tx(context.formatPhrase(RenderingContext.BUNDLE_REV_EXCP, e.getMessage()) + " ");
118            }
119          }
120          root.blockquote().para().addChildren(xn);
121        }
122      }
123    }
124    return false;
125  }
126 
127
128  private boolean renderDocument(XhtmlNode x, ResourceWrapper b, List<BaseWrapper> entries) throws UnsupportedEncodingException, FHIRException, IOException, EOperationOutcome {
129    // from the spec:
130    //
131    // When the document is presented for human consumption, applications SHOULD present the collated narrative portions in order:
132    // * The subject resource Narrative
133    // * The Composition resource Narrative
134    // * The section.text Narratives
135    ResourceWrapper comp = (ResourceWrapper) entries.get(0).getChildByName("resource").getAsResource();
136    ResourceWrapper subject = resolveReference(entries, comp.get("subject"));
137    if (subject != null) {
138      if (subject.hasNarrative()) {
139        x.addChildren(subject.getNarrative());        
140      } else {
141        RendererFactory.factory(subject, context, new ResourceContext(rcontext, subject)).render(x, subject);
142      }
143    }
144    x.hr();
145    if (comp.hasNarrative()) {
146      x.addChildren(comp.getNarrative());
147      x.hr();
148    }
149    List<BaseWrapper> sections = comp.children("section");
150    for (BaseWrapper section : sections) {
151      addSection(x, section, 2, false);
152    }
153    return false;
154  }
155
156  private void addSection(XhtmlNode x, BaseWrapper section, int level, boolean nested) throws UnsupportedEncodingException, FHIRException, IOException {
157    if (section.has("title") || section.has("code") || section.has("text") || section.has("section")) {
158      XhtmlNode div = x.div();
159      if (section.has("title")) {
160        div.h(level).tx(section.get("title").primitiveValue());        
161      } else if (section.has("code")) {
162        renderBase(div.h(level), section.get("code"));                
163      }
164      if (section.has("text")) {
165        Base narrative = section.get("text");
166        x.addChildren(narrative.getXhtml());
167      }      
168      if (section.has("section")) {
169        List<BaseWrapper> sections = section.children("section");
170        for (BaseWrapper child : sections) {
171          if (nested) {
172            addSection(x.blockquote().para(), child, level+1, true);
173          } else {
174            addSection(x, child, level+1, true);
175          }
176        }
177      }      
178    }
179    // children
180  }
181
182  private ResourceWrapper resolveReference(List<BaseWrapper> entries, Base base) throws UnsupportedEncodingException, FHIRException, IOException {
183    if (base == null) {
184      return null;
185    }
186    Property prop = base.getChildByName("reference");
187    if (prop.hasValues()) {
188      String ref = prop.getValues().get(0).primitiveValue();
189      if (ref != null) {
190        for (BaseWrapper entry : entries) {
191          if (entry.has("fullUrl")) {
192            String fu = entry.get("fullUrl").primitiveValue();
193            if (ref.equals(fu)) {
194              return (ResourceWrapper) entry.getChildByName("resource").getAsResource();
195            }
196          }
197        }
198      }
199    }
200    return null;
201  }
202
203  private boolean renderDocument(XhtmlNode x, Bundle b) throws UnsupportedEncodingException, FHIRException, IOException, EOperationOutcome {
204    // from the spec:
205    //
206    // When the document is presented for human consumption, applications SHOULD present the collated narrative portions in order:
207    // * The subject resource Narrative
208    // * The Composition resource Narrative
209    // * The section.text Narratives
210    Composition comp = (Composition) b.getEntry().get(0).getResource();
211    Resource subject = resolveReference(b, comp.getSubjectFirstRep());
212    if (subject != null) {
213      XhtmlNode nx = (subject instanceof DomainResource) ? ((DomainResource) subject).getText().getDiv() : null;
214      if (nx != null && !nx.isEmpty()) {
215        x.addChildren(nx);        
216      } else {
217        RendererFactory.factory(subject, context).setRcontext(new ResourceContext(rcontext, subject)).render(x, subject);
218      }
219    }
220    x.hr();
221    if (!comp.getText().hasDiv()) {
222      ResourceRenderer rr = RendererFactory.factory(comp, getContext());     
223      rr.setRcontext(new ResourceContext(rcontext, comp));
224      rr.render(comp);
225    }
226    if (comp.getText().hasDiv()) {
227      x.addChildren(comp.getText().getDiv());
228      x.hr();    
229    }
230    for (SectionComponent section : comp.getSection()) {
231      addSection(x, section, 2, false, comp);
232    }
233    return false;
234  }
235
236  private Resource resolveReference(Bundle bnd, Reference reference) {
237    String ref = reference.getReference();
238    if (ref == null) {
239      return null;
240    }
241    for (BundleEntryComponent be : bnd.getEntry()) {
242      if (ref.equals(be.getFullUrl())) {
243        return be.getResource();
244      }
245    }
246    return null;
247  }
248
249
250  private void addSection(XhtmlNode x, SectionComponent section, int level, boolean nested, Composition c) throws UnsupportedEncodingException, FHIRException, IOException {
251    if (section.hasTitle() || section.hasCode() || section.hasText() || section.hasSection()) {
252      XhtmlNode div = x.div();
253      if (section.hasTitle()) {
254        div.h(level).tx(section.getTitle());        
255      } else if (section.hasCode()) {
256        renderBase(div.h(level), section.getCode());                
257      }
258      if (section.hasText()) {
259        x.addChildren(section.getText().getDiv());
260      } 
261      if (section.hasEntry()) {
262        XhtmlNode ul = x.ul();
263        for (Reference r : section.getEntry()) {
264          renderReference(c, ul.li(), r);
265        }
266      }
267      if (section.hasSection()) {
268        List<SectionComponent> sections = section.getSection();
269        for (SectionComponent child : sections) {
270          if (nested) {
271            addSection(x.blockquote().para(), child, level+1, true, c);
272          } else {
273            addSection(x, child, level+1, true, c);            
274          }
275        }
276      }      
277    }
278    // children
279  }
280
281  
282  public XhtmlNode render(Bundle b) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome {
283    if ((b.getType() == BundleType.COLLECTION && allEntresAreHistoryProvenance(b))) {
284      return null;
285    } else {
286      int start = 0;
287      boolean docMode = false;
288      XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
289      if (b.getType() == BundleType.DOCUMENT) {
290        if (!b.hasEntry() || !(b.getEntryFirstRep().hasResource() && b.getEntryFirstRep().getResource() instanceof Composition)) {
291          throw new FHIRException(context.formatPhrase(RenderingContext.BUNDLE_REV_INV_DOC));
292        }
293        renderDocument(x, b);
294        start = 1;
295        docMode = true;
296        x.hr();
297        x.h2().addText(formatPhrase(RenderingContext.BUNDLE_DOCUMENT_CONTENT, b.getId(), b.getType().toCode()));
298      } else {
299        x.para().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_ROOT, b.getId(), b.getType().toCode()));
300      }
301      int i = 0;
302      for (BundleEntryComponent be : b.getEntry()) {
303        i++;
304        if (i > start) {
305          if (be.hasFullUrl())
306            x.an(makeInternalBundleLink(be.getFullUrl()));
307          if (be.hasResource()) {
308            if (be.getResource().hasId()) {
309              x.an(be.getResource().getResourceType().name() + "_" + be.getResource().getId());
310              x.an("hc"+be.getResource().getResourceType().name() + "_" + be.getResource().getId());
311            } else {
312              String id = makeIdFromBundleEntry(be.getFullUrl());
313              x.an(be.getResource().getResourceType().name() + "_" + id);
314              x.an("hc"+be.getResource().getResourceType().name() + "_" + id);
315            }
316          }
317          x.hr();
318          if (docMode) {
319            if (be.hasFullUrl() && be.hasResource()) {
320              x.para().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_DOC_ENTRY_URD, Integer.toString(i), be.getFullUrl(), be.getResource().fhirType(), be.getResource().getIdBase()));
321            } else if (be.hasFullUrl()) {
322              x.para().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_DOC_ENTRY_U, Integer.toString(i), be.getFullUrl()));
323            } else if (be.hasResource()) {
324              x.para().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_DOC_ENTRY_RD, Integer.toString(i), be.getResource().fhirType(), be.getResource().getIdBase()));              
325            }
326          } else {
327            if (be.hasFullUrl()) {
328              x.para().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_ENTRY_URL, Integer.toString(i), be.getFullUrl()));
329            } else {
330              x.para().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_ENTRY, Integer.toString(i)));
331            }
332            if (be.hasRequest())
333              renderRequest(x, be.getRequest());
334            if (be.hasSearch())
335              renderSearch(x, be.getSearch());
336            if (be.hasResponse())
337              renderResponse(x, be.getResponse());
338          }
339          if (be.hasResource()) {
340            if (!docMode) {
341              x.para().addText(formatPhrase(RenderingContext.BUNDLE_RESOURCE, be.getResource().fhirType()));
342            }
343            if (be.hasResource()) {
344              XhtmlNode xn = null;
345              if (be.getResource() instanceof DomainResource) {
346                DomainResource dr = (DomainResource) be.getResource();
347                xn = dr.getText().getDiv();
348              }
349              if (xn == null || xn.isEmpty()) {
350                ResourceRenderer rr = RendererFactory.factory(be.getResource(), context);
351                try {
352                  rr.setRcontext(new ResourceContext(rcontext, be.getResource()));
353                  xn = rr.build(be.getResource());
354                } catch (Exception e) {
355                  xn = makeExceptionXhtml(e, context.formatPhrase(RenderingContext.BUND_REND_GEN_NARR));
356                }
357              }
358              x.blockquote().para().getChildNodes().addAll(checkInternalLinks(b, xn.getChildNodes()));
359            }
360          }
361        }
362      }
363      return x;
364    }
365  }
366
367  public static boolean allEntriesAreHistoryProvenance(List<BaseWrapper> entries) throws UnsupportedEncodingException, FHIRException, IOException {
368    for (BaseWrapper be : entries) {
369      if (!"Provenance".equals(be.get("resource").fhirType())) {
370        return false;
371      }
372    }
373    return !entries.isEmpty();
374  }
375  
376 
377  private boolean allEntresAreHistoryProvenance(Bundle b) {
378    for (BundleEntryComponent be : b.getEntry()) {
379      if (!(be.getResource() instanceof Provenance)) {
380        return false;
381      }
382    }
383    return !b.getEntry().isEmpty();
384  }
385
386  private List<XhtmlNode> checkInternalLinks(Bundle b, List<XhtmlNode> childNodes) {
387    scanNodesForInternalLinks(b, childNodes);
388    return childNodes;
389  }
390
391  private void scanNodesForInternalLinks(Bundle b, List<XhtmlNode> nodes) {
392    for (XhtmlNode n : nodes) {
393      if ("a".equals(n.getName()) && n.hasAttribute("href")) {
394        scanInternalLink(b, n);
395      }
396      scanNodesForInternalLinks(b, n.getChildNodes());
397    }
398  }
399
400  private void scanInternalLink(Bundle b, XhtmlNode n) {
401    boolean fix = false;
402    for (BundleEntryComponent be : b.getEntry()) {
403      if (be.hasFullUrl() && be.getFullUrl().equals(n.getAttribute("href"))) {
404        fix = true;
405      }
406    }
407    if (fix) {
408      n.setAttribute("href", "#"+makeInternalBundleLink(n.getAttribute("href")));
409    }
410  }
411
412  private void renderSearch(XhtmlNode root, BundleEntrySearchComponent search) {
413    StringBuilder b = new StringBuilder();
414    b.append(formatPhrase(RenderingContext.BUNDLE_SEARCH));
415    if (search.hasMode())
416      b.append(formatPhrase(RenderingContext.BUNDLE_SEARCH_MODE, search.getMode().toCode()));
417    if (search.hasScore()) {
418      if (search.hasMode())
419        b.append(",");
420      b.append(formatPhrase(RenderingContext.BUNDLE_SEARCH_SCORE, search.getScore()));
421    }
422    root.para().addText(b.toString());    
423  }
424
425  private void renderResponse(XhtmlNode root, BundleEntryResponseComponent response) {
426    root.para().addText(formatPhrase(RenderingContext.BUNDLE_RESPONSE));
427    StringBuilder b = new StringBuilder();
428    b.append(response.getStatus()+"\r\n");
429    if (response.hasLocation())
430      b.append(formatPhrase(RenderingContext.BUNDLE_LOCATION, response.getLocation())+"\r\n");
431    if (response.hasEtag())
432      b.append(formatPhrase(RenderingContext.BUNDLE_ETAG, response.getEtag())+"\r\n");
433    if (response.hasLastModified())
434      b.append(formatPhrase(RenderingContext.BUNDLE_LAST_MOD, response.getEtag())+"\r\n");
435    root.pre().addText(b.toString());    
436  }
437
438  private void renderRequest(XhtmlNode root, BundleEntryRequestComponent request) {
439    root.para().addText(formatPhrase(RenderingContext.BUNDLE_REQUEST));
440    StringBuilder b = new StringBuilder();
441    b.append(request.getMethod()+" "+request.getUrl()+"\r\n");
442    if (request.hasIfNoneMatch())
443      b.append(formatPhrase(RenderingContext.BUNDLE_IF_NON_MATCH, request.getIfNoneMatch())+"\r\n");
444    if (request.hasIfModifiedSince())
445      b.append(formatPhrase(RenderingContext.BUNDLE_IF_MOD, request.getIfModifiedSince())+"\r\n");
446    if (request.hasIfMatch())
447      b.append(formatPhrase(RenderingContext.BUNDLE_IF_MATCH, request.getIfMatch())+"\r\n");
448    if (request.hasIfNoneExist())
449      b.append(formatPhrase(RenderingContext.BUNDLE_IF_NONE, request.getIfNoneExist())+"\r\n");
450    root.pre().addText(b.toString());    
451  }
452
453
454  public String display(Bundle bundle) throws UnsupportedEncodingException, IOException {
455    return "??";
456  }
457
458  public boolean canRender(Bundle b) {
459    for (BundleEntryComponent be : b.getEntry()) {
460      if (be.hasResource()) {          
461        ResourceRenderer rr = RendererFactory.factory(be.getResource(), context);
462        if (!rr.canRender(be.getResource())) {
463          return false;
464        }
465      }
466    }
467    return true;
468  }
469
470}