001package org.hl7.fhir.r5.renderers;
002
003import java.io.IOException;
004import java.io.UnsupportedEncodingException;
005import java.util.ArrayList;
006import java.util.List;
007
008import org.hl7.fhir.exceptions.DefinitionException;
009import org.hl7.fhir.exceptions.FHIRException;
010import org.hl7.fhir.exceptions.FHIRFormatError;
011import org.hl7.fhir.r5.model.Bundle;
012import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent;
013import org.hl7.fhir.r5.model.Provenance;
014import org.hl7.fhir.r5.renderers.utils.RenderingContext;
015import org.hl7.fhir.r5.renderers.utils.ResourceWrapper;
016import org.hl7.fhir.r5.utils.EOperationOutcome;
017import org.hl7.fhir.utilities.xhtml.NodeType;
018import org.hl7.fhir.utilities.xhtml.XhtmlNode;
019
020public class BundleRenderer extends ResourceRenderer {
021
022
023  public BundleRenderer(RenderingContext context) { 
024    super(context); 
025  } 
026 
027  @Override
028  public String buildSummary(ResourceWrapper r) throws UnsupportedEncodingException, IOException {
029    return context.formatPhrase(RenderingContext.BUNDLE_SUMMARY, getTranslatedCode(r.child("type")), r.children("entry").size());
030  }
031
032  public BundleRenderer setMultiLangMode(boolean multiLangMode) {
033    this.multiLangMode = multiLangMode;
034    return this;
035  }
036  
037  @Override
038  public void buildNarrative(RenderingStatus status, XhtmlNode x, ResourceWrapper b) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome {
039
040    List<ResourceWrapper> entries = b.children("entry");
041    if ("collection".equals(b.primitiveValue("type")) && allEntriesAreHistoryProvenance(entries)) {
042      // nothing
043    } else {
044      int start = 0;
045      XhtmlNode root = x;
046      if ("document".equals(b.primitiveValue("type"))) {
047        if (entries.isEmpty() || (entries.get(0).has("resource") && !"Composition".equals(entries.get(0).child("resource").fhirType())))
048          throw new FHIRException(context.formatPhrase(RenderingContext.BUND_REND_INVALID_DOC, b.getId(), entries.get(0).child("resource").fhirType()+"')"));
049        renderDocument(status, root, b, entries);
050        if (!context.isTechnicalMode()) {
051          return;
052        }
053        start = 1;
054        root.hr();
055        root.h2().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_DOCUMENT_CONTENTS));
056      } else {
057        renderResourceTechDetails(b, x);
058        root.para().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_ROOT, b.getId(), b.primitiveValue("type")));
059      }
060      int i = 0;
061      for (ResourceWrapper be : entries) {
062        i++;
063        if (i >= start) {
064          String link = null;
065          if (be.has("fullUrl")) {
066            link = makeInternalBundleLink(b, be.primitiveValue("fullUrl"));
067            if (!context.hasAnchor(link)) {
068              context.addAnchor(link);
069              root.an(context.prefixAnchor(link));
070            }
071          }
072          ResourceWrapper res = be.child("resource");
073          if (be.has("resource")) {
074            String id = res.has("id") ? res.primitiveValue("id") : makeIdFromBundleEntry(be.primitiveValue("fullUrl"));
075            String anchor = res.fhirType() + "_" + id;
076            if (id != null && !context.hasAnchor(anchor)) {
077              context.addAnchor(anchor);
078              root.an(context.prefixAnchor(anchor));
079            }
080            anchor = "hc"+anchor;
081            if (id != null && !context.hasAnchor(anchor)) {
082              context.addAnchor(anchor);
083              root.an(context.prefixAnchor(anchor));
084            }
085            String ver = res.has("meta") ? res.child("meta").primitiveValue("version") : null;
086            if (ver != null) {
087              if (link != null) {
088                link = link + "/"+ver;
089                if (!context.hasAnchor(link)) {
090                  context.addAnchor(link);
091                  root.an(context.prefixAnchor(link));
092                }
093              }
094              if (id != null) {
095                anchor = anchor + "/"+ver;
096                if (!context.hasAnchor(anchor)) {
097                  context.addAnchor(anchor);
098                  root.an(context.prefixAnchor(anchor));
099                }
100              }
101            }
102          }
103          root.hr();
104          if (be.has("fullUrl")) {
105            root.para().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_ENTRY_URL, Integer.toString(i), be.primitiveValue("fullUrl")));
106          } else {
107            root.para().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_ENTRY, Integer.toString(i)));
108          }
109          if (be.has("search")) {
110            renderSearch(x, be.child("search"));
111          }
112          //        if (be.hasRequest())
113          //          renderRequest(root, be.getRequest());
114          //        if (be.hasSearch())
115          //          renderSearch(root, be.getSearch());
116          //        if (be.hasResponse())
117          //          renderResponse(root, be.getResponse());
118          if (be.has("resource")) {
119            ResourceWrapper r = res;
120            root.para().addText(formatPhrase(RenderingContext.BUNDLE_RESOURCE, r.fhirType()));
121            XhtmlNode xn = r.getNarrative();
122            if (xn == null || xn.isEmpty()) {
123              ResourceRenderer rr = RendererFactory.factory(r, context);
124              try {
125                xn = new XhtmlNode(NodeType.Element, "div"); 
126                rr.buildNarrative(new RenderingStatus(), xn, r);
127              } catch (Exception e) {
128                xn = new XhtmlNode();
129                xn.para().b().tx(context.formatPhrase(RenderingContext.BUNDLE_REV_EXCP, e.getMessage()) + " ");
130              }
131            } else {
132              xn.stripAnchorsByName(context.getAnchors());
133            }
134            root.blockquote().addChildren(xn);
135          }
136          if (be.has("request")) {
137            renderRequest(x, be.child("request"));
138          }
139          if (be.has("response")) {
140            renderResponse(x, be.child("response"));
141          }
142        }
143      }
144    }
145  }
146
147  private void renderDocument(RenderingStatus status, XhtmlNode x, ResourceWrapper b, List<ResourceWrapper> entries) throws UnsupportedEncodingException, FHIRException, IOException, EOperationOutcome {
148    // from the spec:
149    //
150    // When the document is presented for human consumption, applications SHOULD present the collated narrative portions in order:
151    // * The subject resource Narrative
152    // * The Composition resource Narrative
153    // * The section.text Narratives
154
155    ResourceWrapper comp = (ResourceWrapper) entries.get(0).child("resource");
156    
157    XhtmlNode sum = renderResourceTechDetails(b, docSection(x, "Document Details"), comp.primitiveValueMN("title", "name"));
158    List<ResourceWrapper> subjectList = comp.children("subject");
159    if (sum != null) {
160      XhtmlNode p = sum.para();
161      p.startScript("doc");
162      renderDataType(status, p.param("status"), comp.child("status"));
163      renderDataType(status, p.param("date"), comp.child("date"));
164      renderDataTypes(status, p.param("author"), comp.children("author"));
165      renderDataTypes(status, p.param("subject"), subjectList);
166      if (comp.has("encounter")) {
167        renderDataType(status, p.param("encounter"), comp.child("encounter"));
168        p.paramValue("has-encounter", "true");
169      } else {
170        p.paramValue("has-encounter", "false");
171      }
172      p.execScript(context.formatMessage(RenderingContext.DOCUMENT_SUMMARY));
173      p.closeScript();
174
175      // status, type, category, subject, encounter, date, author, 
176      x.hr();
177    }
178
179    List<ResourceWrapper> subjects = resolveReferences(entries, subjectList);
180    int i = 0;
181    for (ResourceWrapper subject : subjects) {
182      XhtmlNode sec = docSection(x, "Document Subject");
183      if (subject != null) {
184        if (subject.hasNarrative()) {
185          sec.addChildren(subject.getNarrative());        
186        } else {
187          RendererFactory.factory(subject, context).buildNarrative(status, sec, subject);
188        }
189      } else {
190        sec.para().b().tx("Unable to resolve subject '"+displayReference(subjects.get(i))+"'");
191      }
192      i++;
193    }
194    x.hr();
195    XhtmlNode sec = docSection(x, "Document Content");
196    if (comp.hasNarrative()) {
197      sec.addChildren(comp.getNarrative());
198      sec.hr();
199    }
200    List<ResourceWrapper> sections = comp.children("section");
201    for (ResourceWrapper section : sections) {
202      addSection(status, sec, section, 2, false);
203    }
204  }
205
206  private void renderDataTypes(RenderingStatus status, XhtmlNode param, List<ResourceWrapper> children) throws FHIRFormatError, DefinitionException, IOException {
207    if (children != null && !children.isEmpty()) {
208      boolean first = true;
209      for (ResourceWrapper child : children) {
210        if (first) {first = false; } else {param.tx(", "); }
211        renderDataType(status, param, child);
212      }
213    } 
214  }
215
216  private XhtmlNode docSection(XhtmlNode x, String name) {
217    XhtmlNode div = x.div();
218    div.style("border: 1px solid maroon; padding: 10px; background-color: #f2faf9; min-height: 160px;");
219    div.para().b().tx(name);
220    return div;
221  }
222
223  private void addSection(RenderingStatus status, XhtmlNode x, ResourceWrapper section, int level, boolean nested) throws UnsupportedEncodingException, FHIRException, IOException {
224    if (section.has("title") || section.has("code") || section.has("text") || section.has("section")) {
225      XhtmlNode div = x.div();
226      if (section.has("title")) {
227        div.h(level).tx(section.primitiveValue("title"));        
228      } else if (section.has("code")) {
229        renderDataType(status, div.h(level), section.child("code"));                
230      }
231      if (section.has("text")) {
232        ResourceWrapper narrative = section.child("text");
233        ResourceWrapper xh = narrative.child("div");
234        x.addChildren(xh.getXhtml());
235      }      
236      if (section.has("section")) {
237        List<ResourceWrapper> sections = section.children("section");
238        for (ResourceWrapper child : sections) {
239          if (nested) {
240            addSection(status, x.blockquote().para(), child, level+1, true);
241          } else {
242            addSection(status, x, child, level+1, true);
243          }
244        }
245      }      
246    }
247    // children
248  }
249
250  private List<ResourceWrapper> resolveReferences(List<ResourceWrapper> entries, List<ResourceWrapper> baselist) throws UnsupportedEncodingException, FHIRException, IOException {
251    List<ResourceWrapper> list = new ArrayList<>();
252    if (baselist != null) {
253      for (ResourceWrapper base : baselist) {
254        ResourceWrapper res = null;
255        ResourceWrapper prop = base.child("reference");
256        if (prop != null && prop.hasPrimitiveValue()) {
257          for (ResourceWrapper entry : entries) {
258            if (entry.has("fullUrl")) {
259              String fu = entry.primitiveValue("fullUrl");
260              if (prop.primitiveValue().equals(fu)) {
261                res = entry.child("resource");
262              }
263            }
264          }
265          list.add(res);
266        }
267      }
268    }
269    return list;
270  }
271  
272  private ResourceWrapper resolveReference(List<ResourceWrapper> entries, ResourceWrapper base) throws UnsupportedEncodingException, FHIRException, IOException {
273    if (base == null) {
274      return null;
275    }
276    ResourceWrapper prop = base.child("reference");
277    if (prop != null && prop.hasPrimitiveValue()) {
278      for (ResourceWrapper entry : entries) {
279        if (entry.has("fullUrl")) {
280          String fu = entry.primitiveValue("fullUrl");
281          if (prop.primitiveValue().equals(fu)) {
282            return entry.child("resource");
283          }
284        }
285      }
286    }
287    return null;
288  }
289  
290  public static boolean allEntriesAreHistoryProvenance(List<ResourceWrapper> entries) throws UnsupportedEncodingException, FHIRException, IOException {
291    for (ResourceWrapper be : entries) {
292      if (!"Provenance".equals(be.child("resource").fhirType())) {
293        return false;
294      }
295    }
296    return !entries.isEmpty();
297  }
298  
299 
300  private boolean allEntresAreHistoryProvenance(Bundle b) {
301    for (BundleEntryComponent be : b.getEntry()) {
302      if (!(be.getResource() instanceof Provenance)) {
303        return false;
304      }
305    }
306    return !b.getEntry().isEmpty();
307  }
308
309//  private List<XhtmlNode> checkInternalLinks(Bundle b, List<XhtmlNode> childNodes) {
310//    scanNodesForInternalLinks(b, childNodes);
311//    return childNodes;
312//  }
313//
314//  private void scanNodesForInternalLinks(Bundle b, List<XhtmlNode> nodes) {
315//    for (XhtmlNode n : nodes) {
316//      if ("a".equals(n.getName()) && n.hasAttribute("href")) {
317//        scanInternalLink(b, n);
318//      }
319//      scanNodesForInternalLinks(b, n.getChildNodes());
320//    }
321//  }
322//
323//  private void scanInternalLink(Bundle b, XhtmlNode n) {
324//    boolean fix = false;
325//    for (BundleEntryComponent be : b.getEntry()) {
326//      if (be.hasFullUrl() && be.getFullUrl().equals(n.getAttribute("href"))) {
327//        fix = true;
328//      }
329//    }
330//    if (fix) {
331//      n.setAttribute("href", "#"+makeInternalBundleLink(b, n.getAttribute("href")));
332//    }
333//  }
334
335  private void renderSearch(XhtmlNode root, ResourceWrapper search) {
336    StringBuilder b = new StringBuilder();
337    b.append(formatPhrase(RenderingContext.BUNDLE_SEARCH));
338    if (search.has("mode"))
339      b.append(formatPhrase(RenderingContext.BUNDLE_SEARCH_MODE, search.primitiveValue("mode")));
340    if (search.has("score")) {
341      if (search.has("mode")) {
342        b.append(",");
343      }
344      b.append(formatPhrase(RenderingContext.BUNDLE_SEARCH_SCORE, search.primitiveValue("score")));
345    }
346    root.para().addText(b.toString());    
347  }
348
349  private void renderResponse(XhtmlNode root, ResourceWrapper response) {
350    root.para().addText(formatPhrase(RenderingContext.BUNDLE_RESPONSE));
351    StringBuilder b = new StringBuilder();
352    b.append(response.primitiveValue("status")+"\r\n");
353    if (response.has("location"))
354      b.append(formatPhrase(RenderingContext.BUNDLE_LOCATION, response.primitiveValue("location"))+"\r\n");
355    if (response.has("etag"))
356      b.append(formatPhrase(RenderingContext.BUNDLE_ETAG, response.primitiveValue("etag"))+"\r\n");
357    if (response.has("lastModified"))
358      b.append(formatPhrase(RenderingContext.BUNDLE_LAST_MOD, response.primitiveValue("lastModified"))+"\r\n");
359    root.pre().addText(b.toString());    
360  }
361
362  private void renderRequest(XhtmlNode root, ResourceWrapper request) {
363    root.para().addText(formatPhrase(RenderingContext.BUNDLE_REQUEST));
364    StringBuilder b = new StringBuilder();
365    b.append(request.primitiveValue("method")+" "+request.primitiveValue("url")+"\r\n");
366    if (request.has("ifNoneMatch"))
367      b.append(formatPhrase(RenderingContext.BUNDLE_IF_NON_MATCH, request.primitiveValue("ifNoneMatch"))+"\r\n");
368    if (request.has("ifModifiedSince"))
369      b.append(formatPhrase(RenderingContext.BUNDLE_IF_MOD, request.primitiveValue("ifModifiedSince"))+"\r\n");
370    if (request.has("ifMatch"))
371      b.append(formatPhrase(RenderingContext.BUNDLE_IF_MATCH, request.primitiveValue("ifMatch"))+"\r\n");
372    if (request.has("ifNoneExist"))
373      b.append(formatPhrase(RenderingContext.BUNDLE_IF_NONE, request.primitiveValue("ifNoneExist"))+"\r\n");
374    root.pre().addText(b.toString());    
375  }
376
377  public boolean canRender(Bundle b) {
378    for (BundleEntryComponent be : b.getEntry()) {
379      if (be.hasResource()) {          
380        ResourceRenderer rr = RendererFactory.factory(be.getResource(), context);
381        if (!rr.canRender(be.getResource())) {
382          return false;
383        }
384      }
385    }
386    return true;
387  }
388
389}