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