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