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