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