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.xhtml.NodeType; 018import org.hl7.fhir.utilities.xhtml.XhtmlNode; 019 020public class BundleRenderer extends ResourceRenderer { 021 022 023 public BundleRenderer(RenderingContext context) { 024 super(context); 025 } 026 027 @Override 028 public String buildSummary(ResourceWrapper r) throws UnsupportedEncodingException, IOException { 029 return context.formatPhrase(RenderingContext.BUNDLE_SUMMARY, getTranslatedCode(r.child("type")), r.children("entry").size()); 030 } 031 032 public BundleRenderer setMultiLangMode(boolean multiLangMode) { 033 this.multiLangMode = multiLangMode; 034 return this; 035 } 036 037 @Override 038 public void buildNarrative(RenderingStatus status, XhtmlNode x, ResourceWrapper b) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome { 039 040 List<ResourceWrapper> entries = b.children("entry"); 041 if ("collection".equals(b.primitiveValue("type")) && allEntriesAreHistoryProvenance(entries)) { 042 // nothing 043 } else { 044 int start = 0; 045 XhtmlNode root = x; 046 if ("document".equals(b.primitiveValue("type"))) { 047 if (entries.isEmpty() || (entries.get(0).has("resource") && !"Composition".equals(entries.get(0).child("resource").fhirType()))) 048 throw new FHIRException(context.formatPhrase(RenderingContext.BUND_REND_INVALID_DOC, b.getId(), entries.get(0).child("resource").fhirType()+"')")); 049 renderDocument(status, root, b, entries); 050 if (!context.isTechnicalMode()) { 051 return; 052 } 053 start = 1; 054 root.hr(); 055 root.h2().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_DOCUMENT_CONTENTS)); 056 } else { 057 renderResourceTechDetails(b, x); 058 root.para().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_ROOT, b.getId(), b.primitiveValue("type"))); 059 } 060 int i = 0; 061 for (ResourceWrapper be : entries) { 062 i++; 063 if (i >= start) { 064 String link = null; 065 if (be.has("fullUrl")) { 066 link = makeInternalBundleLink(b, be.primitiveValue("fullUrl")); 067 if (!context.hasAnchor(link)) { 068 context.addAnchor(link); 069 root.an(context.prefixAnchor(link)); 070 } 071 } 072 ResourceWrapper res = be.child("resource"); 073 if (be.has("resource")) { 074 String id = res.has("id") ? res.primitiveValue("id") : makeIdFromBundleEntry(be.primitiveValue("fullUrl")); 075 String anchor = res.fhirType() + "_" + id; 076 if (id != null && !context.hasAnchor(anchor)) { 077 context.addAnchor(anchor); 078 root.an(context.prefixAnchor(anchor)); 079 } 080 anchor = "hc"+anchor; 081 if (id != null && !context.hasAnchor(anchor)) { 082 context.addAnchor(anchor); 083 root.an(context.prefixAnchor(anchor)); 084 } 085 String ver = res.has("meta") ? res.child("meta").primitiveValue("version") : null; 086 if (ver != null) { 087 if (link != null) { 088 link = link + "/"+ver; 089 if (!context.hasAnchor(link)) { 090 context.addAnchor(link); 091 root.an(context.prefixAnchor(link)); 092 } 093 } 094 if (id != null) { 095 anchor = anchor + "/"+ver; 096 if (!context.hasAnchor(anchor)) { 097 context.addAnchor(anchor); 098 root.an(context.prefixAnchor(anchor)); 099 } 100 } 101 } 102 } 103 root.hr(); 104 if (be.has("fullUrl")) { 105 root.para().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_ENTRY_URL, Integer.toString(i), be.primitiveValue("fullUrl"))); 106 } else { 107 root.para().addText(formatPhrase(RenderingContext.BUNDLE_HEADER_ENTRY, Integer.toString(i))); 108 } 109 if (be.has("search")) { 110 renderSearch(x, be.child("search")); 111 } 112 // if (be.hasRequest()) 113 // renderRequest(root, be.getRequest()); 114 // if (be.hasSearch()) 115 // renderSearch(root, be.getSearch()); 116 // if (be.hasResponse()) 117 // renderResponse(root, be.getResponse()); 118 if (be.has("resource")) { 119 ResourceWrapper r = res; 120 root.para().addText(formatPhrase(RenderingContext.BUNDLE_RESOURCE, r.fhirType())); 121 XhtmlNode xn = r.getNarrative(); 122 if (xn == null || xn.isEmpty()) { 123 ResourceRenderer rr = RendererFactory.factory(r, context); 124 try { 125 xn = new XhtmlNode(NodeType.Element, "div"); 126 rr.buildNarrative(new RenderingStatus(), xn, r); 127 } catch (Exception e) { 128 xn = new XhtmlNode(); 129 xn.para().b().tx(context.formatPhrase(RenderingContext.BUNDLE_REV_EXCP, e.getMessage()) + " "); 130 } 131 } else { 132 xn.stripAnchorsByName(context.getAnchors()); 133 } 134 root.blockquote().addChildren(xn); 135 } 136 if (be.has("request")) { 137 renderRequest(x, be.child("request")); 138 } 139 if (be.has("response")) { 140 renderResponse(x, be.child("response")); 141 } 142 } 143 } 144 } 145 } 146 147 private void renderDocument(RenderingStatus status, XhtmlNode x, ResourceWrapper b, List<ResourceWrapper> entries) throws UnsupportedEncodingException, FHIRException, IOException, EOperationOutcome { 148 // from the spec: 149 // 150 // When the document is presented for human consumption, applications SHOULD present the collated narrative portions in order: 151 // * The subject resource Narrative 152 // * The Composition resource Narrative 153 // * The section.text Narratives 154 155 ResourceWrapper comp = (ResourceWrapper) entries.get(0).child("resource"); 156 157 XhtmlNode sum = renderResourceTechDetails(b, docSection(x, "Document Details"), comp.primitiveValueMN("title", "name")); 158 List<ResourceWrapper> subjectList = comp.children("subject"); 159 if (sum != null) { 160 XhtmlNode p = sum.para(); 161 p.startScript("doc"); 162 renderDataType(status, p.param("status"), comp.child("status")); 163 renderDataType(status, p.param("date"), comp.child("date")); 164 renderDataTypes(status, p.param("author"), comp.children("author")); 165 renderDataTypes(status, p.param("subject"), subjectList); 166 if (comp.has("encounter")) { 167 renderDataType(status, p.param("encounter"), comp.child("encounter")); 168 p.paramValue("has-encounter", "true"); 169 } else { 170 p.paramValue("has-encounter", "false"); 171 } 172 p.execScript(context.formatMessage(RenderingContext.DOCUMENT_SUMMARY)); 173 p.closeScript(); 174 175 // status, type, category, subject, encounter, date, author, 176 x.hr(); 177 } 178 179 List<ResourceWrapper> subjects = resolveReferences(entries, subjectList); 180 int i = 0; 181 for (ResourceWrapper subject : subjects) { 182 XhtmlNode sec = docSection(x, "Document Subject"); 183 if (subject != null) { 184 if (subject.hasNarrative()) { 185 sec.addChildren(subject.getNarrative()); 186 } else { 187 RendererFactory.factory(subject, context).buildNarrative(status, sec, subject); 188 } 189 } else { 190 sec.para().b().tx("Unable to resolve subject '"+displayReference(subjects.get(i))+"'"); 191 } 192 i++; 193 } 194 x.hr(); 195 XhtmlNode sec = docSection(x, "Document Content"); 196 if (comp.hasNarrative()) { 197 sec.addChildren(comp.getNarrative()); 198 sec.hr(); 199 } 200 List<ResourceWrapper> sections = comp.children("section"); 201 for (ResourceWrapper section : sections) { 202 addSection(status, sec, section, 2, false); 203 } 204 } 205 206 private void renderDataTypes(RenderingStatus status, XhtmlNode param, List<ResourceWrapper> children) throws FHIRFormatError, DefinitionException, IOException { 207 if (children != null && !children.isEmpty()) { 208 boolean first = true; 209 for (ResourceWrapper child : children) { 210 if (first) {first = false; } else {param.tx(", "); } 211 renderDataType(status, param, child); 212 } 213 } 214 } 215 216 private XhtmlNode docSection(XhtmlNode x, String name) { 217 XhtmlNode div = x.div(); 218 div.style("border: 1px solid maroon; padding: 10px; background-color: #f2faf9; min-height: 160px;"); 219 div.para().b().tx(name); 220 return div; 221 } 222 223 private void addSection(RenderingStatus status, XhtmlNode x, ResourceWrapper section, int level, boolean nested) throws UnsupportedEncodingException, FHIRException, IOException { 224 if (section.has("title") || section.has("code") || section.has("text") || section.has("section")) { 225 XhtmlNode div = x.div(); 226 if (section.has("title")) { 227 div.h(level).tx(section.primitiveValue("title")); 228 } else if (section.has("code")) { 229 renderDataType(status, div.h(level), section.child("code")); 230 } 231 if (section.has("text")) { 232 ResourceWrapper narrative = section.child("text"); 233 ResourceWrapper xh = narrative.child("div"); 234 x.addChildren(xh.getXhtml()); 235 } 236 if (section.has("section")) { 237 List<ResourceWrapper> sections = section.children("section"); 238 for (ResourceWrapper child : sections) { 239 if (nested) { 240 addSection(status, x.blockquote().para(), child, level+1, true); 241 } else { 242 addSection(status, x, child, level+1, true); 243 } 244 } 245 } 246 } 247 // children 248 } 249 250 private List<ResourceWrapper> resolveReferences(List<ResourceWrapper> entries, List<ResourceWrapper> baselist) throws UnsupportedEncodingException, FHIRException, IOException { 251 List<ResourceWrapper> list = new ArrayList<>(); 252 if (baselist != null) { 253 for (ResourceWrapper base : baselist) { 254 ResourceWrapper res = null; 255 ResourceWrapper prop = base.child("reference"); 256 if (prop != null && prop.hasPrimitiveValue()) { 257 for (ResourceWrapper entry : entries) { 258 if (entry.has("fullUrl")) { 259 String fu = entry.primitiveValue("fullUrl"); 260 if (prop.primitiveValue().equals(fu)) { 261 res = entry.child("resource"); 262 } 263 } 264 } 265 list.add(res); 266 } 267 } 268 } 269 return list; 270 } 271 272 private ResourceWrapper resolveReference(List<ResourceWrapper> entries, ResourceWrapper base) throws UnsupportedEncodingException, FHIRException, IOException { 273 if (base == null) { 274 return null; 275 } 276 ResourceWrapper prop = base.child("reference"); 277 if (prop != null && prop.hasPrimitiveValue()) { 278 for (ResourceWrapper entry : entries) { 279 if (entry.has("fullUrl")) { 280 String fu = entry.primitiveValue("fullUrl"); 281 if (prop.primitiveValue().equals(fu)) { 282 return entry.child("resource"); 283 } 284 } 285 } 286 } 287 return null; 288 } 289 290 public static boolean allEntriesAreHistoryProvenance(List<ResourceWrapper> entries) throws UnsupportedEncodingException, FHIRException, IOException { 291 for (ResourceWrapper be : entries) { 292 if (!"Provenance".equals(be.child("resource").fhirType())) { 293 return false; 294 } 295 } 296 return !entries.isEmpty(); 297 } 298 299 300 private boolean allEntresAreHistoryProvenance(Bundle b) { 301 for (BundleEntryComponent be : b.getEntry()) { 302 if (!(be.getResource() instanceof Provenance)) { 303 return false; 304 } 305 } 306 return !b.getEntry().isEmpty(); 307 } 308 309// private List<XhtmlNode> checkInternalLinks(Bundle b, List<XhtmlNode> childNodes) { 310// scanNodesForInternalLinks(b, childNodes); 311// return childNodes; 312// } 313// 314// private void scanNodesForInternalLinks(Bundle b, List<XhtmlNode> nodes) { 315// for (XhtmlNode n : nodes) { 316// if ("a".equals(n.getName()) && n.hasAttribute("href")) { 317// scanInternalLink(b, n); 318// } 319// scanNodesForInternalLinks(b, n.getChildNodes()); 320// } 321// } 322// 323// private void scanInternalLink(Bundle b, XhtmlNode n) { 324// boolean fix = false; 325// for (BundleEntryComponent be : b.getEntry()) { 326// if (be.hasFullUrl() && be.getFullUrl().equals(n.getAttribute("href"))) { 327// fix = true; 328// } 329// } 330// if (fix) { 331// n.setAttribute("href", "#"+makeInternalBundleLink(b, n.getAttribute("href"))); 332// } 333// } 334 335 private void renderSearch(XhtmlNode root, ResourceWrapper search) { 336 StringBuilder b = new StringBuilder(); 337 b.append(formatPhrase(RenderingContext.BUNDLE_SEARCH)); 338 if (search.has("mode")) 339 b.append(formatPhrase(RenderingContext.BUNDLE_SEARCH_MODE, search.primitiveValue("mode"))); 340 if (search.has("score")) { 341 if (search.has("mode")) { 342 b.append(","); 343 } 344 b.append(formatPhrase(RenderingContext.BUNDLE_SEARCH_SCORE, search.primitiveValue("score"))); 345 } 346 root.para().addText(b.toString()); 347 } 348 349 private void renderResponse(XhtmlNode root, ResourceWrapper response) { 350 root.para().addText(formatPhrase(RenderingContext.BUNDLE_RESPONSE)); 351 StringBuilder b = new StringBuilder(); 352 b.append(response.primitiveValue("status")+"\r\n"); 353 if (response.has("location")) 354 b.append(formatPhrase(RenderingContext.BUNDLE_LOCATION, response.primitiveValue("location"))+"\r\n"); 355 if (response.has("etag")) 356 b.append(formatPhrase(RenderingContext.BUNDLE_ETAG, response.primitiveValue("etag"))+"\r\n"); 357 if (response.has("lastModified")) 358 b.append(formatPhrase(RenderingContext.BUNDLE_LAST_MOD, response.primitiveValue("lastModified"))+"\r\n"); 359 root.pre().addText(b.toString()); 360 } 361 362 private void renderRequest(XhtmlNode root, ResourceWrapper request) { 363 root.para().addText(formatPhrase(RenderingContext.BUNDLE_REQUEST)); 364 StringBuilder b = new StringBuilder(); 365 b.append(request.primitiveValue("method")+" "+request.primitiveValue("url")+"\r\n"); 366 if (request.has("ifNoneMatch")) 367 b.append(formatPhrase(RenderingContext.BUNDLE_IF_NON_MATCH, request.primitiveValue("ifNoneMatch"))+"\r\n"); 368 if (request.has("ifModifiedSince")) 369 b.append(formatPhrase(RenderingContext.BUNDLE_IF_MOD, request.primitiveValue("ifModifiedSince"))+"\r\n"); 370 if (request.has("ifMatch")) 371 b.append(formatPhrase(RenderingContext.BUNDLE_IF_MATCH, request.primitiveValue("ifMatch"))+"\r\n"); 372 if (request.has("ifNoneExist")) 373 b.append(formatPhrase(RenderingContext.BUNDLE_IF_NONE, request.primitiveValue("ifNoneExist"))+"\r\n"); 374 root.pre().addText(b.toString()); 375 } 376 377 public boolean canRender(Bundle b) { 378 for (BundleEntryComponent be : b.getEntry()) { 379 if (be.hasResource()) { 380 ResourceRenderer rr = RendererFactory.factory(be.getResource(), context); 381 if (!rr.canRender(be.getResource())) { 382 return false; 383 } 384 } 385 } 386 return true; 387 } 388 389}