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