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