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