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              ResourceRenderer rr = RendererFactory.factory(r, context);
129              try {
130                xn = new XhtmlNode(NodeType.Element, "div"); 
131                rr.buildNarrative(new RenderingStatus(), xn, r);
132              } catch (Exception e) {
133                xn = new XhtmlNode();
134                xn.para().b().tx(context.formatPhrase(RenderingContext.BUNDLE_REV_EXCP, e.getMessage()) + " ");
135              }
136            } else {
137              xn.stripAnchorsByName(context.getAnchors());
138            }
139            root.blockquote().addChildren(xn);
140          }
141          if (be.has("request")) {
142            renderRequest(x, be.child("request"));
143          }
144          if (be.has("response")) {
145            renderResponse(x, be.child("response"));
146          }
147        }
148      }
149    }
150  }
151
152  private void renderDocument(RenderingStatus status, XhtmlNode x, ResourceWrapper b, List<ResourceWrapper> entries, List<ResourceWrapper> filter) throws UnsupportedEncodingException, FHIRException, IOException, EOperationOutcome {
153    
154    // from the spec:
155    //
156    // When the document is presented for human consumption, applications SHOULD present the collated narrative portions in order:
157    // * The subject resource Narrative
158    // * The Composition resource Narrative
159    // * The section.text Narratives
160
161    ResourceWrapper comp = (ResourceWrapper) entries.get(0).child("resource");
162    filter.add(entries.get(0));
163    
164    XhtmlNode sum = renderResourceTechDetails(b, docSection(x, formatPhrase(RenderingI18nContext.BUNDLE_DOCUMENT_DETAILS)), comp.primitiveValueMN("title", "name"));
165    List<ResourceWrapper> subjectList = comp.children("subject");
166    if (sum != null) {
167      XhtmlNode p = sum.para();
168      p.startScript("doc");
169      renderDataType(status, p.param("status"), comp.child("status"));
170      renderDataType(status, p.param("date"), comp.child("date"));
171      renderDataTypes(status, p.param("author"), comp.children("author"));
172      renderDataTypes(status, p.param("subject"), subjectList);
173      if (comp.has("encounter")) {
174        renderDataType(status, p.param("encounter"), comp.child("encounter"));
175        p.paramValue("has-encounter", "true");
176      } else {
177        p.paramValue("has-encounter", "false");
178      }
179      p.execScript(context.formatMessage(RenderingContext.DOCUMENT_SUMMARY));
180      p.closeScript();
181
182      // status, type, category, subject, encounter, date, author, 
183      x.hr();
184    }
185
186    List<ResourceWrapper> subjects = resolveReferences(entries, subjectList, filter);
187    int i = 0;
188    for (ResourceWrapper subject : subjects) {
189      XhtmlNode sec = docSection(x, "Document Subject");
190      if (subject != null) {
191        if (subject.hasNarrative()) {
192          sec.addChildren(subject.getNarrative());        
193        } else {
194          RendererFactory.factory(subject, context).buildNarrative(status, sec, subject);
195        }
196      } else {
197        sec.para().b().tx("Unable to resolve subject '"+displayReference(subjects.get(i))+"'");
198      }
199      i++;
200    }
201    x.hr();
202    XhtmlNode sec = docSection(x, formatPhrase(RenderingI18nContext.BUNDLE_DOCUMENT_CONTENT));
203    if (comp.hasNarrative()) {
204      sec.addChildren(comp.getNarrative());
205      sec.hr();
206    }
207    List<ResourceWrapper> sections = comp.children("section");
208    for (ResourceWrapper section : sections) {
209      addSection(status, sec, section, 2, false);
210    }
211  }
212
213  private void renderDataTypes(RenderingStatus status, XhtmlNode param, List<ResourceWrapper> children) throws FHIRFormatError, DefinitionException, IOException {
214    if (children != null && !children.isEmpty()) {
215      boolean first = true;
216      for (ResourceWrapper child : children) {
217        if (first) {first = false; } else {param.tx(", "); }
218        renderDataType(status, param, child);
219      }
220    } 
221  }
222
223  private XhtmlNode docSection(XhtmlNode x, String name) {
224    XhtmlNode div = x.div();
225    div.style("border: 1px solid maroon; padding: 10px; background-color: #f2faf9; min-height: 160px;");
226    div.para().b().tx(name);
227    return div;
228  }
229
230  private void addSection(RenderingStatus status, XhtmlNode x, ResourceWrapper section, int level, boolean nested) throws UnsupportedEncodingException, FHIRException, IOException {
231    if (section.has("title") || section.has("code") || section.has("text") || section.has("section")) {
232      XhtmlNode div = x.div();
233      if (section.has("title")) {
234        div.h(level).tx(section.primitiveValue("title"));        
235      } else if (section.has("code")) {
236        renderDataType(status, div.h(level), section.child("code"));                
237      }
238      if (section.has("text")) {
239        ResourceWrapper narrative = section.child("text");
240        ResourceWrapper xh = narrative.child("div");
241        x.addChildren(xh.getXhtml());
242      }      
243      if (section.has("section")) {
244        List<ResourceWrapper> sections = section.children("section");
245        for (ResourceWrapper child : sections) {
246          if (nested) {
247            addSection(status, x.blockquote().para(), child, level+1, true);
248          } else {
249            addSection(status, x, child, level+1, true);
250          }
251        }
252      }      
253    }
254    // children
255  }
256
257  private List<ResourceWrapper> resolveReferences(List<ResourceWrapper> entries, List<ResourceWrapper> baselist, List<ResourceWrapper> filter) throws UnsupportedEncodingException, FHIRException, IOException {
258    List<ResourceWrapper> list = new ArrayList<>();
259    if (baselist != null) {
260      for (ResourceWrapper base : baselist) {
261        ResourceWrapper res = null;
262        ResourceWrapper prop = base.child("reference");
263        if (prop != null && prop.hasPrimitiveValue()) {
264          String ref = prop.primitiveValue();
265          for (ResourceWrapper entry : entries) {
266            if (entry.has("fullUrl")) {
267              String fu = entry.primitiveValue("fullUrl");
268              if (ref.equals(fu)) {
269//                filter.add(entry);
270                res = entry.child("resource");
271              }
272            }
273            if (entry.has("resource")) {
274              String type = entry.child("resource").fhirType();
275              String id = entry.child("resource").primitiveValue("id");
276              if (ref.equals(type+"/"+id)) {
277//                filter.add(entry);
278                res = entry.child("resource");
279              }
280            }
281          }
282          list.add(res);
283        }
284      }
285    }
286    return list;
287  }
288  
289  private ResourceWrapper resolveReference(List<ResourceWrapper> entries, ResourceWrapper base) throws UnsupportedEncodingException, FHIRException, IOException {
290    if (base == null) {
291      return null;
292    }
293    ResourceWrapper prop = base.child("reference");
294    if (prop != null && prop.hasPrimitiveValue()) {
295      for (ResourceWrapper entry : entries) {
296        if (entry.has("fullUrl")) {
297          String fu = entry.primitiveValue("fullUrl");
298          if (prop.primitiveValue().equals(fu)) {
299            return entry.child("resource");
300          }
301        }
302      }
303    }
304    return null;
305  }
306  
307  public static boolean allEntriesAreHistoryProvenance(List<ResourceWrapper> entries) throws UnsupportedEncodingException, FHIRException, IOException {
308    for (ResourceWrapper be : entries) {
309      if (!be.has("child") || !"Provenance".equals(be.child("resource").fhirType())) {
310        return false;
311      }
312    }
313    return !entries.isEmpty();
314  }
315  
316 
317  private boolean allEntresAreHistoryProvenance(Bundle b) {
318    for (BundleEntryComponent be : b.getEntry()) {
319      if (!(be.getResource() instanceof Provenance)) {
320        return false;
321      }
322    }
323    return !b.getEntry().isEmpty();
324  }
325
326//  private List<XhtmlNode> checkInternalLinks(Bundle b, List<XhtmlNode> childNodes) {
327//    scanNodesForInternalLinks(b, childNodes);
328//    return childNodes;
329//  }
330//
331//  private void scanNodesForInternalLinks(Bundle b, List<XhtmlNode> nodes) {
332//    for (XhtmlNode n : nodes) {
333//      if ("a".equals(n.getName()) && n.hasAttribute("href")) {
334//        scanInternalLink(b, n);
335//      }
336//      scanNodesForInternalLinks(b, n.getChildNodes());
337//    }
338//  }
339//
340//  private void scanInternalLink(Bundle b, XhtmlNode n) {
341//    boolean fix = false;
342//    for (BundleEntryComponent be : b.getEntry()) {
343//      if (be.hasFullUrl() && be.getFullUrl().equals(n.getAttribute("href"))) {
344//        fix = true;
345//      }
346//    }
347//    if (fix) {
348//      n.setAttribute("href", "#"+makeInternalBundleLink(b, n.getAttribute("href")));
349//    }
350//  }
351
352  private void renderSearch(XhtmlNode root, ResourceWrapper search) {
353    StringBuilder b = new StringBuilder();
354    b.append(formatPhrase(RenderingContext.BUNDLE_SEARCH));
355    if (search.has("mode"))
356      b.append(formatPhrase(RenderingContext.BUNDLE_SEARCH_MODE, search.primitiveValue("mode")));
357    if (search.has("score")) {
358      if (search.has("mode")) {
359        b.append(",");
360      }
361      b.append(formatPhrase(RenderingContext.BUNDLE_SEARCH_SCORE, search.primitiveValue("score")));
362    }
363    root.para().addText(b.toString());    
364  }
365
366  private void renderResponse(XhtmlNode root, ResourceWrapper response) {
367    root.para().addText(formatPhrase(RenderingContext.BUNDLE_RESPONSE));
368    StringBuilder b = new StringBuilder();
369    b.append(response.primitiveValue("status")+"\r\n");
370    if (response.has("location"))
371      b.append(formatPhrase(RenderingContext.BUNDLE_LOCATION, response.primitiveValue("location"))+"\r\n");
372    if (response.has("etag"))
373      b.append(formatPhrase(RenderingContext.BUNDLE_ETAG, response.primitiveValue("etag"))+"\r\n");
374    if (response.has("lastModified"))
375      b.append(formatPhrase(RenderingContext.BUNDLE_LAST_MOD, response.primitiveValue("lastModified"))+"\r\n");
376    root.pre().addText(b.toString());    
377  }
378
379  private void renderRequest(XhtmlNode root, ResourceWrapper request) {
380    root.para().addText(formatPhrase(RenderingContext.BUNDLE_REQUEST));
381    StringBuilder b = new StringBuilder();
382    b.append(request.primitiveValue("method")+" "+request.primitiveValue("url")+"\r\n");
383    if (request.has("ifNoneMatch"))
384      b.append(formatPhrase(RenderingContext.BUNDLE_IF_NON_MATCH, request.primitiveValue("ifNoneMatch"))+"\r\n");
385    if (request.has("ifModifiedSince"))
386      b.append(formatPhrase(RenderingContext.BUNDLE_IF_MOD, request.primitiveValue("ifModifiedSince"))+"\r\n");
387    if (request.has("ifMatch"))
388      b.append(formatPhrase(RenderingContext.BUNDLE_IF_MATCH, request.primitiveValue("ifMatch"))+"\r\n");
389    if (request.has("ifNoneExist"))
390      b.append(formatPhrase(RenderingContext.BUNDLE_IF_NONE, request.primitiveValue("ifNoneExist"))+"\r\n");
391    root.pre().addText(b.toString());    
392  }
393
394  public boolean canRender(Bundle b) {
395    for (BundleEntryComponent be : b.getEntry()) {
396      if (be.hasResource()) {          
397        ResourceRenderer rr = RendererFactory.factory(be.getResource(), context);
398        if (!rr.canRender(be.getResource())) {
399          return false;
400        }
401      }
402    }
403    return true;
404  }
405
406}