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}