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