001package org.hl7.fhir.r4b.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.r4b.elementmodel.Element; 011import org.hl7.fhir.r4b.model.Base; 012import org.hl7.fhir.r4b.model.Bundle.BundleEntryComponent; 013import org.hl7.fhir.r4b.model.CodeSystem.ConceptDefinitionComponent; 014import org.hl7.fhir.r4b.model.Coding; 015import org.hl7.fhir.r4b.model.Enumerations.PublicationStatus; 016import org.hl7.fhir.r4b.model.CanonicalResource; 017import org.hl7.fhir.r4b.model.CodeSystem; 018import org.hl7.fhir.r4b.model.DomainResource; 019import org.hl7.fhir.r4b.model.Narrative; 020import org.hl7.fhir.r4b.model.Narrative.NarrativeStatus; 021import org.hl7.fhir.r4b.model.Reference; 022import org.hl7.fhir.r4b.model.Resource; 023import org.hl7.fhir.r4b.renderers.utils.BaseWrappers.BaseWrapper; 024import org.hl7.fhir.r4b.renderers.utils.BaseWrappers.PropertyWrapper; 025import org.hl7.fhir.r4b.renderers.utils.BaseWrappers.ResourceWrapper; 026import org.hl7.fhir.r4b.renderers.utils.DirectWrappers.ResourceWrapperDirect; 027import org.hl7.fhir.r4b.renderers.utils.ElementWrappers.ResourceWrapperMetaElement; 028import org.hl7.fhir.r4b.renderers.utils.RenderingContext; 029import org.hl7.fhir.r4b.renderers.utils.Resolver.ResourceContext; 030import org.hl7.fhir.r4b.renderers.utils.Resolver.ResourceWithReference; 031import org.hl7.fhir.r4b.terminologies.CodeSystemUtilities; 032import org.hl7.fhir.r4b.utils.EOperationOutcome; 033import org.hl7.fhir.r4b.utils.ToolingExtensions; 034import org.hl7.fhir.r4b.utils.XVerExtensionManager; 035import org.hl7.fhir.utilities.Utilities; 036import org.hl7.fhir.utilities.xhtml.NodeType; 037import org.hl7.fhir.utilities.xhtml.XhtmlNode; 038 039public abstract class ResourceRenderer extends DataRenderer { 040 041 protected ResourceContext rcontext; 042 protected XVerExtensionManager xverManager; 043 protected boolean forResource; 044 045 046 public ResourceRenderer(RenderingContext context) { 047 super(context); 048 } 049 050 public ResourceRenderer(RenderingContext context, ResourceContext rcontext) { 051 super(context); 052 this.rcontext = rcontext; 053 } 054 055 public XhtmlNode build(Resource dr) throws FHIRFormatError, DefinitionException, FHIRException, IOException, EOperationOutcome { 056 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 057 render(x, dr); 058 return x; 059 } 060 /** 061 * given a resource, update it's narrative with the best rendering available 062 * 063 * @param r - the domain resource in question 064 * 065 * @throws IOException 066 * @throws EOperationOutcome 067 * @throws FHIRException 068 */ 069 070 public void render(DomainResource r) throws IOException, FHIRException, EOperationOutcome { 071 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 072 boolean ofr = forResource; 073 boolean hasExtensions; 074 try { 075 forResource = true; 076 hasExtensions = render(x, r); 077 } finally { 078 forResource = ofr; 079 } 080 inject(r, x, hasExtensions ? NarrativeStatus.EXTENSIONS : NarrativeStatus.GENERATED); 081 } 082 083 public XhtmlNode render(ResourceWrapper r) throws IOException, FHIRException, EOperationOutcome { 084 assert r.getContext() == context; 085 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 086 boolean hasExtensions = render(x, r); 087 if (r.hasNarrative()) { 088 r.injectNarrative(x, hasExtensions ? NarrativeStatus.EXTENSIONS : NarrativeStatus.GENERATED); 089 } 090 return x; 091 } 092 093 public abstract boolean render(XhtmlNode x, Resource r) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome; 094 095 public boolean render(XhtmlNode x, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome { 096 ProfileDrivenRenderer pr = new ProfileDrivenRenderer(context); 097 return pr.render(x, r); 098 } 099 100 public void describe(XhtmlNode x, Resource r) throws UnsupportedEncodingException, IOException { 101 x.tx(display(r)); 102 } 103 104 public void describe(XhtmlNode x, ResourceWrapper r) throws UnsupportedEncodingException, IOException { 105 x.tx(display(r)); 106 } 107 108 public abstract String display(Resource r) throws UnsupportedEncodingException, IOException; 109 public abstract String display(ResourceWrapper r) throws UnsupportedEncodingException, IOException; 110 111 public static void inject(DomainResource r, XhtmlNode x, NarrativeStatus status) { 112 if (!x.hasAttribute("xmlns")) 113 x.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); 114 if (r.hasLanguage()) { 115 // use both - see https://www.w3.org/TR/i18n-html-tech-lang/#langvalues 116 x.setAttribute("lang", r.getLanguage()); 117 x.setAttribute("xml:lang", r.getLanguage()); 118 } 119 r.getText().setUserData("renderer.generated", true); 120 if (!r.hasText() || !r.getText().hasDiv() || r.getText().getDiv().getChildNodes().isEmpty()) { 121 r.setText(new Narrative()); 122 r.getText().setDiv(x); 123 r.getText().setStatus(status); 124 } else { 125 XhtmlNode n = r.getText().getDiv(); 126 n.clear(); 127 n.getChildNodes().addAll(x.getChildNodes()); 128 } 129 } 130 131 public void renderCanonical(Resource res, XhtmlNode x, String url) throws UnsupportedEncodingException, IOException { 132 ResourceWrapper rw = new ResourceWrapperDirect(this.context, res); 133 renderCanonical(rw, x, url); 134 } 135 136 public void renderCanonical(ResourceWrapper rw, XhtmlNode x, String url) throws UnsupportedEncodingException, IOException { 137 renderCanonical(rw, x, url, true); 138 } 139 140 public void renderCanonical(ResourceWrapper rw, XhtmlNode x, String url, boolean allowLinks) throws UnsupportedEncodingException, IOException { 141 if (url == null) { 142 return; 143 } 144 Resource target = context.getWorker().fetchResource(Resource.class, url); 145 if (target == null || !(target instanceof CanonicalResource)) { 146 x.code().tx(url); 147 } else { 148 CanonicalResource cr = (CanonicalResource) target; 149 if (url.contains("|")) { 150 if (target.hasUserData("path")) { 151 x.ah(target.getUserString("path")).tx(cr.present()+" (version "+cr.getVersion()+")"); 152 } else { 153 url = url.substring(0, url.indexOf("|")); 154 x.code().tx(url); 155 x.tx(": "+cr.present()+" (version "+cr.getVersion()+")"); 156 } 157 } else { 158 if (target.hasUserData("path")) { 159 x.ah(target.getUserString("path")).tx(cr.present()); 160 } else { 161 url = url.substring(0, url.indexOf("|")); 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.r4b.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}