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