
001package org.hl7.fhir.r5.renderers; 002 003import java.io.IOException; 004import java.io.UnsupportedEncodingException; 005 006import org.hl7.fhir.exceptions.DefinitionException; 007import org.hl7.fhir.exceptions.FHIRException; 008import org.hl7.fhir.exceptions.FHIRFormatError; 009import org.hl7.fhir.r5.elementmodel.Element; 010import org.hl7.fhir.r5.model.Base; 011import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent; 012import org.hl7.fhir.r5.model.CanonicalResource; 013import org.hl7.fhir.r5.model.CodeSystem; 014import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; 015import org.hl7.fhir.r5.model.CodeableReference; 016import org.hl7.fhir.r5.model.Coding; 017import org.hl7.fhir.r5.model.DataType; 018import org.hl7.fhir.r5.model.DomainResource; 019import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; 020import org.hl7.fhir.r5.model.Narrative; 021import org.hl7.fhir.r5.model.Narrative.NarrativeStatus; 022import org.hl7.fhir.r5.model.Reference; 023import org.hl7.fhir.r5.model.Resource; 024import org.hl7.fhir.r5.renderers.utils.BaseWrappers.BaseWrapper; 025import org.hl7.fhir.r5.renderers.utils.BaseWrappers.PropertyWrapper; 026import org.hl7.fhir.r5.renderers.utils.BaseWrappers.ResourceWrapper; 027import org.hl7.fhir.r5.renderers.utils.DirectWrappers.ResourceWrapperDirect; 028import org.hl7.fhir.r5.renderers.utils.ElementWrappers.ResourceWrapperMetaElement; 029import org.hl7.fhir.r5.renderers.utils.RenderingContext; 030import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceContext; 031import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceWithReference; 032import org.hl7.fhir.r5.terminologies.CodeSystemUtilities; 033import org.hl7.fhir.r5.utils.EOperationOutcome; 034import org.hl7.fhir.r5.utils.ToolingExtensions; 035import org.hl7.fhir.r5.utils.XVerExtensionManager; 036import org.hl7.fhir.utilities.Utilities; 037import org.hl7.fhir.utilities.xhtml.NodeType; 038import org.hl7.fhir.utilities.xhtml.XhtmlNode; 039 040public abstract class ResourceRenderer extends DataRenderer { 041 042 public enum RendererType { 043 NATIVE, PROFILE, LIQUID 044 045 } 046 047 protected ResourceContext rcontext; 048 protected XVerExtensionManager xverManager; 049 050 051 public ResourceRenderer(RenderingContext context) { 052 super(context); 053 } 054 055 public ResourceRenderer(RenderingContext context, ResourceContext rcontext) { 056 super(context); 057 this.rcontext = rcontext; 058 } 059 060 public ResourceContext getRcontext() { 061 return rcontext; 062 } 063 064 public ResourceRenderer setRcontext(ResourceContext rcontext) { 065 this.rcontext = rcontext; 066 return this; 067 } 068 069 public XhtmlNode build(Resource dr) throws FHIRFormatError, DefinitionException, FHIRException, IOException, EOperationOutcome { 070 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 071 render(x, dr); 072 return x; 073 } 074 /** 075 * given a resource, update it's narrative with the best rendering available 076 * 077 * @param r - the domain resource in question 078 * 079 * @throws IOException 080 * @throws EOperationOutcome 081 * @throws FHIRException 082 */ 083 084 public void render(DomainResource r) throws IOException, FHIRException, EOperationOutcome { 085 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 086 boolean hasExtensions; 087 hasExtensions = render(x, r); 088 inject(r, x, hasExtensions ? NarrativeStatus.EXTENSIONS : NarrativeStatus.GENERATED); 089 } 090 091 public XhtmlNode render(ResourceWrapper r) throws IOException, FHIRException, EOperationOutcome { 092 assert r.getContext() == context; 093 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 094 boolean hasExtensions = render(x, r); 095 if (r.hasNarrative()) { 096 r.injectNarrative(x, hasExtensions ? NarrativeStatus.EXTENSIONS : NarrativeStatus.GENERATED); 097 } 098 return x; 099 } 100 101 public abstract boolean render(XhtmlNode x, Resource r) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome; 102 103 public boolean render(XhtmlNode x, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome { 104 ProfileDrivenRenderer pr = new ProfileDrivenRenderer(context); 105 return pr.render(x, r); 106 } 107 108 public void describe(XhtmlNode x, Resource r) throws UnsupportedEncodingException, IOException { 109 x.tx(display(r)); 110 } 111 112 public void describe(XhtmlNode x, ResourceWrapper r) throws UnsupportedEncodingException, IOException { 113 x.tx(display(r)); 114 } 115 116 public abstract String display(Resource r) throws UnsupportedEncodingException, IOException; 117 public abstract String display(ResourceWrapper r) throws UnsupportedEncodingException, IOException; 118 119 public static void inject(DomainResource r, XhtmlNode x, NarrativeStatus status) { 120 if (!x.hasAttribute("xmlns")) 121 x.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); 122 if (r.hasLanguage()) { 123 // use both - see https://www.w3.org/TR/i18n-html-tech-lang/#langvalues 124 x.setAttribute("lang", r.getLanguage()); 125 x.setAttribute("xml:lang", r.getLanguage()); 126 } 127 r.getText().setUserData("renderer.generated", true); 128 if (!r.hasText() || !r.getText().hasDiv() || r.getText().getDiv().getChildNodes().isEmpty()) { 129 r.setText(new Narrative()); 130 r.getText().setDiv(x); 131 r.getText().setStatus(status); 132 } else { 133 XhtmlNode n = r.getText().getDiv(); 134 n.clear(); 135 n.getChildNodes().addAll(x.getChildNodes()); 136 } 137 } 138 139 public void renderCanonical(Resource res, XhtmlNode x, String url) throws UnsupportedEncodingException, IOException { 140 ResourceWrapper rw = new ResourceWrapperDirect(this.context, res); 141 renderCanonical(rw, x, url); 142 } 143 144 public void renderCanonical(ResourceWrapper rw, XhtmlNode x, String url) throws UnsupportedEncodingException, IOException { 145 renderCanonical(rw, x, url, true, rw.getResource()); 146 } 147 148 public void renderCanonical(ResourceWrapper rw, XhtmlNode x, String url, boolean allowLinks, Resource src) throws UnsupportedEncodingException, IOException { 149 if (url == null) { 150 return; 151 } 152 Resource target = context.getWorker().fetchResource(Resource.class, url, src); 153 if (target == null || !(target instanceof CanonicalResource)) { 154 x.code().tx(url); 155 } else { 156 CanonicalResource cr = (CanonicalResource) target; 157 if (url.contains("|")) { 158 if (target.hasUserData("path")) { 159 x.ah(target.getUserString("path")).tx(cr.present()+" (version "+cr.getVersion()+")"); 160 } else { 161 url = url.substring(0, url.indexOf("|")); 162 x.code().tx(url); 163 x.tx(": "+cr.present()+" (version "+cr.getVersion()+")"); 164 } 165 } else { 166 if (target.hasUserData("path")) { 167 x.ah(target.getUserString("path")).tx(cr.present()); 168 } else { 169 x.code().tx(url); 170 x.tx(" ("+cr.present()+")"); 171 } 172 } 173 } 174 } 175 176 public void render(Resource res, XhtmlNode x, DataType type) throws FHIRFormatError, DefinitionException, IOException { 177 if (type instanceof Reference) { 178 renderReference(res, x, (Reference) type); 179 } else if (type instanceof CodeableReference) { 180 CodeableReference cr = (CodeableReference) type; 181 if (cr.hasReference()) { 182 renderReference(res, x, cr.getReference()); 183 } else { 184 render(x, type); 185 } 186 } else { 187 render(x, type); 188 } 189 } 190 191 public void render(ResourceWrapper res, XhtmlNode x, DataType type) throws FHIRFormatError, DefinitionException, IOException { 192 if (type instanceof Reference) { 193 renderReference(res, x, (Reference) type); 194 } else if (type instanceof CodeableReference) { 195 CodeableReference cr = (CodeableReference) type; 196 if (cr.hasReference()) { 197 renderReference(res, x, cr.getReference()); 198 } else { 199 render(x, type); 200 } 201 } else { 202 render(x, type); 203 } 204 } 205 206 public void renderReference(Resource res, XhtmlNode x, Reference r) throws UnsupportedEncodingException, IOException { 207 ResourceWrapper rw = new ResourceWrapperDirect(this.context, res); 208 renderReference(rw, x, r); 209 } 210 211 public void renderReference(ResourceWrapper rw, XhtmlNode x, Reference r) throws UnsupportedEncodingException, IOException { 212 renderReference(rw, x, r, true); 213 } 214 215 public void renderReference(ResourceWrapper rw, XhtmlNode x, Reference r, boolean allowLinks) throws UnsupportedEncodingException, IOException { 216 if (r == null) { 217 x.tx("null!"); 218 return; 219 } 220 XhtmlNode c = null; 221 ResourceWithReference tr = null; 222 if (r.hasReferenceElement() && allowLinks) { 223 tr = resolveReference(rw, r.getReference()); 224 225 if (!r.getReference().startsWith("#")) { 226 if (tr != null && tr.getReference() != null) 227 c = x.ah(tr.getReference()); 228 else 229 c = x.ah(r.getReference()); 230 } else { 231 232 c = x.ah(r.getReference()); 233 } 234 } else { 235 c = x.span(null, null); 236 } 237 if (tr != null && tr.getReference() != null && tr.getReference().startsWith("#")) { 238 c.tx("See above ("); 239 } 240 // what to display: if text is provided, then that. if the reference was resolved, then show the name, or the generated narrative 241 String display = r.hasDisplayElement() ? r.getDisplay() : null; 242 String name = tr != null && tr.getResource() != null ? tr.getResource().getNameFromResource() : null; 243 244 if (display == null && (tr == null || tr.getResource() == null)) { 245 c.addText(r.getReference()); 246 } else if (context.isTechnicalMode()) { 247 c.addText(r.getReference()); 248 if (display != null) { 249 c.addText(": "+display); 250 } 251 if ((tr == null || (tr.getReference() != null && !tr.getReference().startsWith("#"))) && name != null) { 252 x.addText(" \""+name+"\""); 253 } 254 if (r.hasExtension(ToolingExtensions.EXT_TARGET_ID)) { 255 x.addText("(#"+r.getExtensionString(ToolingExtensions.EXT_TARGET_ID)+")"); 256 } else if (r.hasExtension(ToolingExtensions.EXT_TARGET_PATH)) { 257 x.addText("(#/"+r.getExtensionString(ToolingExtensions.EXT_TARGET_PATH)+")"); 258 } 259 } else { 260 if (display != null) { 261 c.addText(display); 262 } else if (name != null) { 263 c.addText(name); 264 } else { 265 c.tx(". Generated Summary: "); 266 if (tr != null) { 267 new ProfileDrivenRenderer(context).generateResourceSummary(c, tr.getResource(), true, r.getReference().startsWith("#"), true); 268 } 269 } 270 } 271 if (tr != null && tr.getReference() != null && tr.getReference().startsWith("#")) { 272 c.tx(")"); 273 } 274 } 275 276 public void renderReference(ResourceWrapper rw, XhtmlNode x, BaseWrapper r) throws UnsupportedEncodingException, IOException { 277 XhtmlNode c = x; 278 ResourceWithReference tr = null; 279 String v; 280 if (r.has("reference")) { 281 v = r.get("reference").primitiveValue(); 282 tr = resolveReference(rw, v); 283 284 if (!v.startsWith("#")) { 285 if (tr != null && tr.getReference() != null) 286 c = x.ah(tr.getReference()); 287 else 288 c = x.ah(v); 289 } 290 } else { 291 v = ""; 292 } 293 // what to display: if text is provided, then that. if the reference was resolved, then show the generated narrative 294 if (r.has("display")) { 295 c.addText(r.get("display").primitiveValue()); 296 if (tr != null && tr.getResource() != null) { 297 c.tx(". Generated Summary: "); 298 new ProfileDrivenRenderer(context).generateResourceSummary(c, tr.getResource(), true, v.startsWith("#"), false); 299 } 300 } else if (tr != null && tr.getResource() != null) { 301 new ProfileDrivenRenderer(context).generateResourceSummary(c, tr.getResource(), v.startsWith("#"), v.startsWith("#"), false); 302 } else { 303 c.addText(v); 304 } 305 } 306 307 protected ResourceWithReference resolveReference(ResourceWrapper res, String url) { 308 if (url == null) 309 return null; 310 if (url.startsWith("#") && res != null) { 311 for (ResourceWrapper r : res.getContained()) { 312 if (r.getId().equals(url.substring(1))) 313 return new ResourceWithReference(null, r); 314 } 315 return null; 316 } 317 String version = null; 318 if (url.contains("/_history/")) { 319 version = url.substring(url.indexOf("/_history/")+10); 320 url = url.substring(0, url.indexOf("/_history/")); 321 } 322 323 if (rcontext != null) { 324 BundleEntryComponent bundleResource = rcontext.resolve(url); 325 if (bundleResource != null) { 326 String id = bundleResource.getResource().getId(); 327 if (id == null) { 328 id = makeIdFromBundleEntry(bundleResource.getFullUrl()); 329 } 330 String bundleUrl = "#" + bundleResource.getResource().getResourceType().name() + "_" + id; 331 return new ResourceWithReference(bundleUrl, new ResourceWrapperDirect(this.context, bundleResource.getResource())); 332 } 333 org.hl7.fhir.r5.elementmodel.Element bundleElement = rcontext.resolveElement(url, version); 334 if (bundleElement != null) { 335 String bundleUrl = null; 336 Element br = bundleElement.getNamedChild("resource"); 337 if (br.getChildValue("id") != null) { 338 bundleUrl = "#" + br.fhirType() + "_" + br.getChildValue("id"); 339 } else { 340 bundleUrl = "#" +fullUrlToAnchor(bundleElement.getChildValue("fullUrl")); 341 } 342 return new ResourceWithReference(bundleUrl, new ResourceWrapperMetaElement(this.context, br)); 343 } 344 } 345 346 Resource ae = getContext().getWorker().fetchResource(null, url, version); 347 if (ae != null) 348 return new ResourceWithReference(url, new ResourceWrapperDirect(this.context, ae)); 349 else if (context.getResolver() != null) { 350 return context.getResolver().resolve(context, url); 351 } else 352 return null; 353 } 354 355 356 protected String makeIdFromBundleEntry(String url) { 357 if (url == null) { 358 return null; 359 } 360 if (url.startsWith("urn:uuid:")) { 361 return url.substring(9).toLowerCase(); 362 } 363 return fullUrlToAnchor(url); 364 } 365 366 private String fullUrlToAnchor(String url) { 367 return url.replace(":", "").replace("/", "_"); 368 } 369 370 protected void generateCopyright(XhtmlNode x, CanonicalResource cs) { 371 XhtmlNode p = x.para(); 372 p.b().tx(getContext().getWorker().translator().translate("xhtml-gen-cs", "Copyright Statement:", context.getLang())); 373 smartAddText(p, " " + cs.getCopyright()); 374 } 375 376 public String displayReference(Resource res, Reference r) throws UnsupportedEncodingException, IOException { 377 return "todo"; 378 } 379 380 381 public Base parseType(String string, String type) { 382 return null; 383 } 384 385 protected PropertyWrapper getProperty(ResourceWrapper res, String name) { 386 for (PropertyWrapper t : res.children()) { 387 if (t.getName().equals(name)) 388 return t; 389 } 390 return null; 391 } 392 393 protected PropertyWrapper getProperty(BaseWrapper res, String name) { 394 for (PropertyWrapper t : res.children()) { 395 if (t.getName().equals(name)) 396 return t; 397 } 398 return null; 399 } 400 401 protected boolean valued(PropertyWrapper pw) { 402 return pw != null && pw.hasValues(); 403 } 404 405 406 protected ResourceWrapper fetchResource(BaseWrapper subject) throws UnsupportedEncodingException, FHIRException, IOException { 407 if (context.getResolver() == null) 408 return null; 409 410 PropertyWrapper ref = subject.getChildByName("reference"); 411 if (ref == null || !ref.hasValues()) { 412 return null; 413 } 414 String url = ref.value().getBase().primitiveValue(); 415 ResourceWithReference rr = context.getResolver().resolve(context, url); 416 return rr == null ? null : rr.getResource(); 417 } 418 419 420 protected String describeStatus(PublicationStatus status, boolean experimental) { 421 switch (status) { 422 case ACTIVE: return experimental ? "Experimental" : "Active"; 423 case DRAFT: return "draft"; 424 case RETIRED: return "retired"; 425 default: return "Unknown"; 426 } 427 } 428 429 protected void renderCommitteeLink(XhtmlNode x, CanonicalResource cr) { 430 String code = ToolingExtensions.readStringExtension(cr, ToolingExtensions.EXT_WORKGROUP); 431 CodeSystem cs = context.getWorker().fetchCodeSystem("http://terminology.hl7.org/CodeSystem/hl7-work-group"); 432 if (cs == null || !cs.hasUserData("path")) 433 x.tx(code); 434 else { 435 ConceptDefinitionComponent cd = CodeSystemUtilities.findCode(cs.getConcept(), code); 436 if (cd == null) { 437 x.tx(code); 438 } else { 439 x.ah(cs.getUserString("path")+"#"+cs.getId()+"-"+cd.getCode()).tx(cd.getDisplay()); 440 } 441 } 442 } 443 444 public static String makeInternalBundleLink(String fullUrl) { 445 return fullUrl.replace(":", "-"); 446 } 447 448 public boolean canRender(Resource resource) { 449 return true; 450 } 451 452 protected void renderResourceHeader(ResourceWrapper r, XhtmlNode x, boolean doId) throws UnsupportedEncodingException, FHIRException, IOException { 453 XhtmlNode div = x.div().style("display: inline-block").style("background-color: #d9e0e7").style("padding: 6px") 454 .style("margin: 4px").style("border: 1px solid #8da1b4") 455 .style("border-radius: 5px").style("line-height: 60%"); 456 457 String id = getPrimitiveValue(r, "id"); 458 if (doId) { 459 div.an(id); 460 } 461 462 String lang = getPrimitiveValue(r, "language"); 463 String ir = getPrimitiveValue(r, "implicitRules"); 464 BaseWrapper meta = r.getChildByName("meta").hasValues() ? r.getChildByName("meta").getValues().get(0) : null; 465 String versionId = getPrimitiveValue(meta, "versionId"); 466 String lastUpdated = getPrimitiveValue(meta, "lastUpdated"); 467 String source = getPrimitiveValue(meta, "source"); 468 469 if (id != null || lang != null || versionId != null || lastUpdated != null) { 470 XhtmlNode p = plateStyle(div.para()); 471 p.tx("Resource "); 472 p.tx(r.fhirType()); 473 p.tx(" "); 474 if (id != null) { 475 p.tx("\""+id+"\" "); 476 } 477 if (versionId != null) { 478 p.tx("Version \""+versionId+"\" "); 479 } 480 if (lastUpdated != null) { 481 p.tx("Updated \""); 482 renderDateTime(p, lastUpdated); 483 p.tx("\" "); 484 } 485 if (lang != null) { 486 p.tx(" (Language \""+lang+"\") "); 487 } 488 } 489 if (ir != null) { 490 plateStyle(div.para()).b().tx("Special rules apply: "+ir+"!"); 491 } 492 if (source != null) { 493 plateStyle(div.para()).tx("Information Source: "+source+"!"); 494 } 495 if (meta != null) { 496 PropertyWrapper pl = meta.getChildByName("profile"); 497 if (pl.hasValues()) { 498 XhtmlNode p = plateStyle(div.para()); 499 p.tx(Utilities.pluralize("Profile", pl.getValues().size())+": "); 500 boolean first = true; 501 for (BaseWrapper bw : pl.getValues()) { 502 if (first) first = false; else p.tx(", "); 503 renderCanonical(r, p, bw.getBase().primitiveValue()); 504 } 505 } 506 PropertyWrapper tl = meta.getChildByName("tag"); 507 if (tl.hasValues()) { 508 XhtmlNode p = plateStyle(div.para()); 509 p.tx(Utilities.pluralize("Tag", tl.getValues().size())+": "); 510 boolean first = true; 511 for (BaseWrapper bw : tl.getValues()) { 512 if (first) first = false; else p.tx(", "); 513 String system = getPrimitiveValue(bw, "system"); 514 String version = getPrimitiveValue(bw, "version"); 515 String code = getPrimitiveValue(bw, "system"); 516 String display = getPrimitiveValue(bw, "system"); 517 renderCoding(p, new Coding(system, version, code, display)); 518 } 519 } 520 PropertyWrapper sl = meta.getChildByName("security"); 521 if (sl.hasValues()) { 522 XhtmlNode p = plateStyle(div.para()); 523 p.tx(Utilities.pluralize("Security Label", tl.getValues().size())+": "); 524 boolean first = true; 525 for (BaseWrapper bw : sl.getValues()) { 526 if (first) first = false; else p.tx(", "); 527 String system = getPrimitiveValue(bw, "system"); 528 String version = getPrimitiveValue(bw, "version"); 529 String code = getPrimitiveValue(bw, "system"); 530 String display = getPrimitiveValue(bw, "system"); 531 renderCoding(p, new Coding(system, version, code, display)); 532 } 533 } 534 } 535 536 } 537 538 private XhtmlNode plateStyle(XhtmlNode para) { 539 return para.style("margin-bottom: 0px"); 540 } 541 542 private String getPrimitiveValue(BaseWrapper b, String name) throws UnsupportedEncodingException, FHIRException, IOException { 543 return b != null && b.has(name) && b.getChildByName(name).hasValues() ? b.getChildByName(name).getValues().get(0).getBase().primitiveValue() : null; 544 } 545 546 private String getPrimitiveValue(ResourceWrapper r, String name) throws UnsupportedEncodingException, FHIRException, IOException { 547 return r.has(name) && r.getChildByName(name).hasValues() ? r.getChildByName(name).getValues().get(0).getBase().primitiveValue() : null; 548 } 549 550 public void renderOrError(DomainResource dr) { 551 try { 552 render(dr); 553 } catch (Exception e) { 554 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 555 x.para().tx("Error rendering: "+e.getMessage()); 556 dr.setText(null); 557 inject(dr, x, NarrativeStatus.GENERATED); 558 } 559 560 } 561 562 public RendererType getRendererType() { 563 return RendererType.NATIVE; 564 } 565}