
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 xn = new XhtmlNode(NodeType.Element, "div"); 129 ResourceRenderer rr = RendererFactory.factory(r, context); 130 try { 131 rr.buildNarrative(new RenderingStatus(), xn, r); 132 } catch (Exception e) { 133 xn.para().b().tx(context.formatPhrase(RenderingContext.BUNDLE_REV_EXCP, e.getMessage()) + " "); 134 } 135 } else { 136 xn.stripAnchorsByName(context.getAnchors()); 137 } 138 root.blockquote().addChildren(xn); 139 } 140 if (be.has("request")) { 141 renderRequest(x, be.child("request")); 142 } 143 if (be.has("response")) { 144 renderResponse(x, be.child("response")); 145 } 146 } 147 } 148 } 149 } 150 151 private void renderDocument(RenderingStatus status, XhtmlNode x, ResourceWrapper b, List<ResourceWrapper> entries, List<ResourceWrapper> filter) throws UnsupportedEncodingException, FHIRException, IOException, EOperationOutcome { 152 153 // from the spec: 154 // 155 // When the document is presented for human consumption, applications SHOULD present the collated narrative portions in order: 156 // * The subject resource Narrative 157 // * The Composition resource Narrative 158 // * The section.text Narratives 159 160 ResourceWrapper comp = (ResourceWrapper) entries.get(0).child("resource"); 161 filter.add(entries.get(0)); 162 163 XhtmlNode sum = renderResourceTechDetails(b, docSection(x, formatPhrase(RenderingI18nContext.BUNDLE_DOCUMENT_DETAILS)), comp.primitiveValueMN("title", "name")); 164 List<ResourceWrapper> subjectList = comp.children("subject"); 165 if (sum != null) { 166 XhtmlNode p = sum.para(); 167 p.startScript("doc"); 168 renderDataType(status, p.param("status"), comp.child("status")); 169 renderDataType(status, p.param("date"), comp.child("date")); 170 renderDataTypes(status, p.param("author"), comp.children("author")); 171 renderDataTypes(status, p.param("subject"), subjectList); 172 if (comp.has("encounter")) { 173 renderDataType(status, p.param("encounter"), comp.child("encounter")); 174 p.paramValue("has-encounter", "true"); 175 } else { 176 p.paramValue("has-encounter", "false"); 177 } 178 p.execScript(context.formatMessage(RenderingContext.DOCUMENT_SUMMARY)); 179 p.closeScript(); 180 181 // status, type, category, subject, encounter, date, author, 182 x.hr(); 183 } 184 185 List<ResourceWrapper> subjects = resolveReferences(entries, subjectList, filter); 186 int i = 0; 187 for (ResourceWrapper subject : subjects) { 188 XhtmlNode sec = docSection(x, "Document Subject"); 189 if (subject != null) { 190 if (subject.hasNarrative()) { 191 sec.addChildren(subject.getNarrative()); 192 } else { 193 RendererFactory.factory(subject, context).buildNarrative(status, sec, subject); 194 } 195 } else { 196 sec.para().b().tx("Unable to resolve subject '"+displayReference(subjects.get(i))+"'"); 197 } 198 i++; 199 } 200 x.hr(); 201 XhtmlNode sec = docSection(x, formatPhrase(RenderingI18nContext.BUNDLE_DOCUMENT_CONTENT)); 202 if (comp.hasNarrative()) { 203 sec.addChildren(comp.getNarrative()); 204 sec.hr(); 205 } 206 List<ResourceWrapper> sections = comp.children("section"); 207 for (ResourceWrapper section : sections) { 208 addSection(status, sec, section, 2, false); 209 } 210 } 211 212 private void renderDataTypes(RenderingStatus status, XhtmlNode param, List<ResourceWrapper> children) throws FHIRFormatError, DefinitionException, IOException { 213 if (children != null && !children.isEmpty()) { 214 boolean first = true; 215 for (ResourceWrapper child : children) { 216 if (first) {first = false; } else {param.tx(", "); } 217 renderDataType(status, param, child); 218 } 219 } 220 } 221 222 private XhtmlNode docSection(XhtmlNode x, String name) { 223 XhtmlNode div = x.div(); 224 div.style("border: 1px solid maroon; padding: 10px; background-color: #f2faf9; min-height: 160px;"); 225 div.para().b().tx(name); 226 return div; 227 } 228 229 private void addSection(RenderingStatus status, XhtmlNode x, ResourceWrapper section, int level, boolean nested) throws UnsupportedEncodingException, FHIRException, IOException { 230 if (section.has("title") || section.has("code") || section.has("text") || section.has("section")) { 231 XhtmlNode div = x.div(); 232 if (section.has("title")) { 233 div.h(level).tx(section.primitiveValue("title")); 234 } else if (section.has("code")) { 235 renderDataType(status, div.h(level), section.child("code")); 236 } 237 if (section.has("text")) { 238 ResourceWrapper narrative = section.child("text"); 239 ResourceWrapper xh = narrative.child("div"); 240 x.addChildren(xh.getXhtml()); 241 } 242 if (section.has("section")) { 243 List<ResourceWrapper> sections = section.children("section"); 244 for (ResourceWrapper child : sections) { 245 if (nested) { 246 addSection(status, x.blockquote().para(), child, level+1, true); 247 } else { 248 addSection(status, x, child, level+1, true); 249 } 250 } 251 } 252 } 253 // children 254 } 255 256 private List<ResourceWrapper> resolveReferences(List<ResourceWrapper> entries, List<ResourceWrapper> baselist, List<ResourceWrapper> filter) throws UnsupportedEncodingException, FHIRException, IOException { 257 List<ResourceWrapper> list = new ArrayList<>(); 258 if (baselist != null) { 259 for (ResourceWrapper base : baselist) { 260 ResourceWrapper res = null; 261 ResourceWrapper prop = base.child("reference"); 262 if (prop != null && prop.hasPrimitiveValue()) { 263 String ref = prop.primitiveValue(); 264 for (ResourceWrapper entry : entries) { 265 if (entry.has("fullUrl")) { 266 String fu = entry.primitiveValue("fullUrl"); 267 if (ref.equals(fu)) { 268// filter.add(entry); 269 res = entry.child("resource"); 270 } 271 } 272 if (entry.has("resource")) { 273 String type = entry.child("resource").fhirType(); 274 String id = entry.child("resource").primitiveValue("id"); 275 if (ref.equals(type+"/"+id)) { 276// filter.add(entry); 277 res = entry.child("resource"); 278 } 279 } 280 } 281 list.add(res); 282 } 283 } 284 } 285 return list; 286 } 287 288 private ResourceWrapper resolveReference(List<ResourceWrapper> entries, ResourceWrapper base) throws UnsupportedEncodingException, FHIRException, IOException { 289 if (base == null) { 290 return null; 291 } 292 ResourceWrapper prop = base.child("reference"); 293 if (prop != null && prop.hasPrimitiveValue()) { 294 for (ResourceWrapper entry : entries) { 295 if (entry.has("fullUrl")) { 296 String fu = entry.primitiveValue("fullUrl"); 297 if (prop.primitiveValue().equals(fu)) { 298 return entry.child("resource"); 299 } 300 } 301 } 302 } 303 return null; 304 } 305 306 public static boolean allEntriesAreHistoryProvenance(List<ResourceWrapper> entries) throws UnsupportedEncodingException, FHIRException, IOException { 307 for (ResourceWrapper be : entries) { 308 if (!be.has("child") || !"Provenance".equals(be.child("resource").fhirType())) { 309 return false; 310 } 311 } 312 return !entries.isEmpty(); 313 } 314 315 316 private boolean allEntresAreHistoryProvenance(Bundle b) { 317 for (BundleEntryComponent be : b.getEntry()) { 318 if (!(be.getResource() instanceof Provenance)) { 319 return false; 320 } 321 } 322 return !b.getEntry().isEmpty(); 323 } 324 325// private List<XhtmlNode> checkInternalLinks(Bundle b, List<XhtmlNode> childNodes) { 326// scanNodesForInternalLinks(b, childNodes); 327// return childNodes; 328// } 329// 330// private void scanNodesForInternalLinks(Bundle b, List<XhtmlNode> nodes) { 331// for (XhtmlNode n : nodes) { 332// if ("a".equals(n.getName()) && n.hasAttribute("href")) { 333// scanInternalLink(b, n); 334// } 335// scanNodesForInternalLinks(b, n.getChildNodes()); 336// } 337// } 338// 339// private void scanInternalLink(Bundle b, XhtmlNode n) { 340// boolean fix = false; 341// for (BundleEntryComponent be : b.getEntry()) { 342// if (be.hasFullUrl() && be.getFullUrl().equals(n.getAttribute("href"))) { 343// fix = true; 344// } 345// } 346// if (fix) { 347// n.setAttribute("href", "#"+makeInternalBundleLink(b, n.getAttribute("href"))); 348// } 349// } 350 351 private void renderSearch(XhtmlNode root, ResourceWrapper search) { 352 StringBuilder b = new StringBuilder(); 353 b.append(formatPhrase(RenderingContext.BUNDLE_SEARCH)); 354 if (search.has("mode")) 355 b.append(formatPhrase(RenderingContext.BUNDLE_SEARCH_MODE, search.primitiveValue("mode"))); 356 if (search.has("score")) { 357 if (search.has("mode")) { 358 b.append(","); 359 } 360 b.append(formatPhrase(RenderingContext.BUNDLE_SEARCH_SCORE, search.primitiveValue("score"))); 361 } 362 root.para().addText(b.toString()); 363 } 364 365 private void renderResponse(XhtmlNode root, ResourceWrapper response) { 366 root.para().addText(formatPhrase(RenderingContext.BUNDLE_RESPONSE)); 367 StringBuilder b = new StringBuilder(); 368 b.append(response.primitiveValue("status")+"\r\n"); 369 if (response.has("location")) 370 b.append(formatPhrase(RenderingContext.BUNDLE_LOCATION, response.primitiveValue("location"))+"\r\n"); 371 if (response.has("etag")) 372 b.append(formatPhrase(RenderingContext.BUNDLE_ETAG, response.primitiveValue("etag"))+"\r\n"); 373 if (response.has("lastModified")) 374 b.append(formatPhrase(RenderingContext.BUNDLE_LAST_MOD, response.primitiveValue("lastModified"))+"\r\n"); 375 root.pre().addText(b.toString()); 376 } 377 378 private void renderRequest(XhtmlNode root, ResourceWrapper request) { 379 root.para().addText(formatPhrase(RenderingContext.BUNDLE_REQUEST)); 380 StringBuilder b = new StringBuilder(); 381 b.append(request.primitiveValue("method")+" "+request.primitiveValue("url")+"\r\n"); 382 if (request.has("ifNoneMatch")) 383 b.append(formatPhrase(RenderingContext.BUNDLE_IF_NON_MATCH, request.primitiveValue("ifNoneMatch"))+"\r\n"); 384 if (request.has("ifModifiedSince")) 385 b.append(formatPhrase(RenderingContext.BUNDLE_IF_MOD, request.primitiveValue("ifModifiedSince"))+"\r\n"); 386 if (request.has("ifMatch")) 387 b.append(formatPhrase(RenderingContext.BUNDLE_IF_MATCH, request.primitiveValue("ifMatch"))+"\r\n"); 388 if (request.has("ifNoneExist")) 389 b.append(formatPhrase(RenderingContext.BUNDLE_IF_NONE, request.primitiveValue("ifNoneExist"))+"\r\n"); 390 root.pre().addText(b.toString()); 391 } 392 393 public boolean canRender(Bundle b) { 394 for (BundleEntryComponent be : b.getEntry()) { 395 if (be.hasResource()) { 396 ResourceRenderer rr = RendererFactory.factory(be.getResource(), context); 397 if (!rr.canRender(be.getResource())) { 398 return false; 399 } 400 } 401 } 402 return true; 403 } 404 405}