001package org.hl7.fhir.r5.renderers; 002 003import java.io.IOException; 004import java.util.ArrayList; 005import java.util.HashMap; 006import java.util.List; 007import java.util.Set; 008 009import org.fhir.ucum.Canonical; 010import org.hl7.fhir.exceptions.DefinitionException; 011import org.hl7.fhir.exceptions.FHIRFormatError; 012import org.hl7.fhir.r5.conformance.profile.BindingResolution; 013import org.hl7.fhir.r5.conformance.profile.ProfileKnowledgeProvider; 014import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; 015import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingAdditionalComponent; 016import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingComponent; 017import org.hl7.fhir.r5.model.ActorDefinition; 018import org.hl7.fhir.r5.model.CanonicalType; 019import org.hl7.fhir.r5.model.CodeSystem; 020import org.hl7.fhir.r5.model.Coding; 021import org.hl7.fhir.r5.model.ElementDefinition; 022import org.hl7.fhir.r5.model.Extension; 023import org.hl7.fhir.r5.model.PrimitiveType; 024import org.hl7.fhir.r5.model.StructureDefinition; 025import org.hl7.fhir.r5.model.UsageContext; 026import org.hl7.fhir.r5.model.ValueSet; 027import org.hl7.fhir.r5.renderers.CodeResolver.CodeResolution; 028import org.hl7.fhir.r5.renderers.ObligationsRenderer.ObligationDetail; 029import org.hl7.fhir.r5.renderers.utils.RenderingContext; 030import org.hl7.fhir.r5.utils.PublicationHacker; 031import org.hl7.fhir.r5.utils.ToolingExtensions; 032import org.hl7.fhir.utilities.MarkDownProcessor; 033import org.hl7.fhir.utilities.Utilities; 034import org.hl7.fhir.utilities.VersionUtilities; 035import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator; 036import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Cell; 037import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Piece; 038import org.hl7.fhir.utilities.xhtml.NodeType; 039import org.hl7.fhir.utilities.xhtml.XhtmlComposer; 040import org.hl7.fhir.utilities.xhtml.XhtmlNode; 041import org.hl7.fhir.utilities.xhtml.XhtmlNodeList; 042 043public class ObligationsRenderer { 044 public static class ObligationDetail { 045 private List<String> codes = new ArrayList<>(); 046 private List<String> elementIds = new ArrayList<>(); 047 private List<CanonicalType> actors = new ArrayList<>(); 048 private String doco; 049 private String docoShort; 050 private String filter; 051 private String filterDoco; 052 private List<UsageContext> usage = new ArrayList<>(); 053 private boolean isUnchanged = false; 054 private boolean matched = false; 055 private boolean removed = false; 056 private ValueSet vs; 057 058 private ObligationDetail compare; 059 private int count = 1; 060 061 public ObligationDetail(Extension ext) { 062 for (Extension e: ext.getExtensionsByUrl("code")) { 063 codes.add(e.getValueStringType().toString()); 064 } 065 for (Extension e: ext.getExtensionsByUrl("actor")) { 066 actors.add(e.getValueCanonicalType()); 067 } 068 this.doco = ext.getExtensionString("documentation"); 069 this.docoShort = ext.getExtensionString("shortDoco"); 070 this.filter = ext.getExtensionString("filter"); 071 this.filterDoco = ext.getExtensionString("filterDocumentation"); 072 if (this.filterDoco == null) { 073 this.filterDoco = ext.getExtensionString("filter-desc"); 074 } 075 for (Extension usage : ext.getExtensionsByUrl("usage")) { 076 this.usage.add(usage.getValueUsageContext()); 077 } 078 for (Extension eid : ext.getExtensionsByUrl("elementId")) { 079 this.elementIds.add(eid.getValue().primitiveValue()); 080 } 081 this.isUnchanged = ext.hasUserData(ProfileUtilities.UD_DERIVATION_EQUALS); 082 } 083 084 private String getKey() { 085 // Todo: Consider extending this with content from usageContext if purpose isn't sufficiently differentiating 086 return String.join(",", codes) + Integer.toString(count); 087 } 088 089 private void incrementCount() { 090 count++; 091 } 092 private void setCompare(ObligationDetail match) { 093 compare = match; 094 match.matched = true; 095 } 096 private boolean alreadyMatched() { 097 return matched; 098 } 099 public String getDoco(boolean full) { 100 return full ? doco : docoShort; 101 } 102 public String getCodes() { 103 return String.join(",", codes); 104 } 105 public List<String> getCodeList() { 106 return new ArrayList<String>(codes); 107 } 108 public boolean unchanged() { 109 if (!isUnchanged) 110 return false; 111 if (compare==null) 112 return true; 113 isUnchanged = true; 114 isUnchanged = isUnchanged && ((codes.isEmpty() && compare.codes.isEmpty()) || codes.equals(compare.codes)); 115 isUnchanged = elementIds.equals(compare.elementIds); 116 isUnchanged = isUnchanged && ((actors.isEmpty() && compare.actors.isEmpty()) || actors.equals(compare.actors)); 117 isUnchanged = isUnchanged && ((doco==null && compare.doco==null) || doco.equals(compare.doco)); 118 isUnchanged = isUnchanged && ((docoShort==null && compare.docoShort==null) || docoShort.equals(compare.docoShort)); 119 isUnchanged = isUnchanged && ((filter==null && compare.filter==null) || filter.equals(compare.filter)); 120 isUnchanged = isUnchanged && ((filterDoco==null && compare.filterDoco==null) || filterDoco.equals(compare.filterDoco)); 121 isUnchanged = isUnchanged && ((usage==null && compare.usage==null) || usage.equals(compare.usage)); 122 return isUnchanged; 123 } 124 125 public boolean hasFilter() { 126 return filter != null; 127 } 128 129 public boolean hasUsage() { 130 return !usage.isEmpty(); 131 } 132 133 public String getFilterDesc() { 134 return filterDoco; 135 } 136 137 public String getFilter() { 138 return filter; 139 } 140 141 public List<UsageContext> getUsage() { 142 return usage; 143 } 144 145 public boolean hasActors() { 146 return !actors.isEmpty(); 147 } 148 149 public boolean hasActor(String id) { 150 for (CanonicalType actor: actors) { 151 if (actor.getValue().equals(id)) 152 return true; 153 } 154 return false; 155 } 156 } 157 158 private static String STYLE_UNCHANGED = "opacity: 0.5;"; 159 private static String STYLE_REMOVED = STYLE_UNCHANGED + "text-decoration: line-through;"; 160 161 private List<ObligationDetail> obligations = new ArrayList<>(); 162 private String corePath; 163 private StructureDefinition profile; 164 private String path; 165 private RenderingContext context; 166 private IMarkdownProcessor md; 167 private CodeResolver cr; 168 169 public ObligationsRenderer(String corePath, StructureDefinition profile, String path, RenderingContext context, IMarkdownProcessor md, CodeResolver cr) { 170 this.corePath = corePath; 171 this.profile = profile; 172 this.path = path; 173 this.context = context; 174 this.md = md; 175 this.cr = cr; 176 } 177 178 179 public void seeObligations(ElementDefinition element, String id) { 180 seeObligations(element.getExtension(), null, false, id); 181 } 182 183 public void seeObligations(List<Extension> list) { 184 seeObligations(list, null, false, "$all"); 185 } 186 187 public void seeRootObligations(String eid, List<Extension> list) { 188 seeRootObligations(eid, list, null, false, "$all"); 189 } 190 191 public void seeObligations(List<Extension> list, List<Extension> compList, boolean compare, String id) { 192 HashMap<String, ObligationDetail> compBindings = new HashMap<String, ObligationDetail>(); 193 if (compare && compList!=null) { 194 for (Extension ext : compList) { 195 ObligationDetail abr = obligationDetail(ext); 196 if (compBindings.containsKey(abr.getKey())) { 197 abr.incrementCount(); 198 } 199 compBindings.put(abr.getKey(), abr); 200 } 201 } 202 203 for (Extension ext : list) { 204 ObligationDetail obd = obligationDetail(ext); 205 if ("$all".equals(id) || (obd.hasActor(id))) { 206 if (compare && compList!=null) { 207 ObligationDetail match = null; 208 do { 209 match = compBindings.get(obd.getKey()); 210 if (obd.alreadyMatched()) 211 obd.incrementCount(); 212 } while (match!=null && obd.alreadyMatched()); 213 if (match!=null) 214 obd.setCompare(match); 215 obligations.add(obd); 216 if (obd.compare!=null) 217 compBindings.remove(obd.compare.getKey()); 218 } else { 219 obligations.add(obd); 220 } 221 } 222 } 223 for (ObligationDetail b: compBindings.values()) { 224 b.removed = true; 225 obligations.add(b); 226 } 227 } 228 229 public void seeRootObligations(String eid, List<Extension> list, List<Extension> compList, boolean compare, String id) { 230 HashMap<String, ObligationDetail> compBindings = new HashMap<String, ObligationDetail>(); 231 if (compare && compList!=null) { 232 for (Extension ext : compList) { 233 if (forElement(eid, ext)) { 234 ObligationDetail abr = obligationDetail(ext); 235 if (compBindings.containsKey(abr.getKey())) { 236 abr.incrementCount(); 237 } 238 compBindings.put(abr.getKey(), abr); 239 } 240 } 241 } 242 243 for (Extension ext : list) { 244 if (forElement(eid, ext)) { 245 ObligationDetail obd = obligationDetail(ext); 246 obd.elementIds.clear(); 247 if ("$all".equals(id) || (obd.hasActor(id))) { 248 if (compare && compList!=null) { 249 ObligationDetail match = null; 250 do { 251 match = compBindings.get(obd.getKey()); 252 if (obd.alreadyMatched()) 253 obd.incrementCount(); 254 } while (match!=null && obd.alreadyMatched()); 255 if (match!=null) 256 obd.setCompare(match); 257 obligations.add(obd); 258 if (obd.compare!=null) 259 compBindings.remove(obd.compare.getKey()); 260 } else { 261 obligations.add(obd); 262 } 263 } 264 } 265 } 266 for (ObligationDetail b: compBindings.values()) { 267 b.removed = true; 268 obligations.add(b); 269 } 270 } 271 272 273 private boolean forElement(String eid, Extension ext) { 274 275 for (Extension exid : ext.getExtensionsByUrl("elementId")) { 276 if (eid.equals(exid.getValue().primitiveValue())) { 277 return true; 278 } 279 } 280 return false; 281 } 282 283 284 protected ObligationDetail obligationDetail(Extension ext) { 285 ObligationDetail abr = new ObligationDetail(ext); 286 return abr; 287 } 288 289 public String render(String defPath, String anchorPrefix, List<ElementDefinition> inScopeElements) throws IOException { 290 if (obligations.isEmpty()) { 291 return ""; 292 } else { 293 XhtmlNode tbl = new XhtmlNode(NodeType.Element, "table"); 294 tbl.attribute("class", "grid"); 295 renderTable(tbl.getChildNodes(), true, defPath, anchorPrefix, inScopeElements); 296 return new XhtmlComposer(false).compose(tbl); 297 } 298 } 299 300 public void renderTable(HierarchicalTableGenerator gen, Cell c, List<ElementDefinition> inScopeElements) throws FHIRFormatError, DefinitionException, IOException { 301 if (obligations.isEmpty()) { 302 return; 303 } else { 304 Piece piece = gen.new Piece("table").attr("class", "grid"); 305 c.getPieces().add(piece); 306 renderTable(piece.getChildren(), false, gen.getDefPath(), gen.getAnchorPrefix(), inScopeElements); 307 } 308 } 309 310 public void renderList(HierarchicalTableGenerator gen, Cell c) throws FHIRFormatError, DefinitionException, IOException { 311 if (obligations.size() > 0) { 312 Piece p = gen.new Piece(null); 313 c.addPiece(p); 314 if (obligations.size() == 1) { 315 renderObligationLI(p.getChildren(), obligations.get(0)); 316 } else { 317 XhtmlNode ul = p.getChildren().ul(); 318 for (ObligationDetail ob : obligations) { 319 renderObligationLI(ul.li().getChildNodes(), ob); 320 } 321 } 322 } 323 } 324 325 private void renderObligationLI(XhtmlNodeList children, ObligationDetail ob) throws IOException { 326 renderCodes(children, ob.getCodeList()); 327 if (ob.hasFilter() || ob.hasUsage() || !ob.elementIds.isEmpty()) { 328 children.tx(" ("); 329 boolean ffirst = !ob.hasFilter(); 330 boolean firstEid = true; 331 332 for (String eid: ob.elementIds) { 333 if (firstEid) { 334 children.span().i().tx("Elements: "); 335 firstEid = false; 336 } else 337 children.tx(", "); 338 String trimmedElement = eid.substring(eid.indexOf(".")+ 1); 339 children.tx(trimmedElement); 340 } 341 if (ob.hasFilter()) { 342 children.span(null, ob.getFilterDesc()).code().tx(ob.getFilter()); 343 } 344 for (UsageContext uc : ob.getUsage()) { 345 if (ffirst) ffirst = false; else children.tx(","); 346 if (!uc.getCode().is("http://terminology.hl7.org/CodeSystem/usage-context-type", "jurisdiction")) { 347 children.tx(displayForUsage(uc.getCode())); 348 children.tx("="); 349 } 350 CodeResolution ccr = this.cr.resolveCode(uc.getValueCodeableConcept()); 351 children.ah(ccr.getLink(), ccr.getHint()).tx(ccr.getDisplay()); 352 } 353 children.tx(")"); 354 } 355 // usage 356 // filter 357 // process 358 } 359 360 361 public void renderTable(List<XhtmlNode> children, boolean fullDoco, String defPath, String anchorPrefix, List<ElementDefinition> inScopeElements) throws FHIRFormatError, DefinitionException, IOException { 362 boolean doco = false; 363 boolean usage = false; 364 boolean actor = false; 365 boolean filter = false; 366 boolean elementId = false; 367 for (ObligationDetail binding : obligations) { 368 actor = actor || !binding.actors.isEmpty() || (binding.compare!=null && !binding.compare.actors.isEmpty()); 369 doco = doco || binding.getDoco(fullDoco)!=null || (binding.compare!=null && binding.compare.getDoco(fullDoco)!=null); 370 usage = usage || !binding.usage.isEmpty() || (binding.compare!=null && !binding.compare.usage.isEmpty()); 371 filter = filter || binding.filter != null || (binding.compare!=null && binding.compare.filter!=null); 372 elementId = elementId || !binding.elementIds.isEmpty() || (binding.compare!=null && !binding.compare.elementIds.isEmpty()); 373 } 374 375 List<String> inScopePaths = new ArrayList<>(); 376 for (ElementDefinition e: inScopeElements) { 377 inScopePaths.add(e.getPath()); 378 } 379 380 XhtmlNode tr = new XhtmlNode(NodeType.Element, "tr"); 381 children.add(tr); 382 tr.td().style("font-size: 11px").b().tx(context.formatPhrase(RenderingContext.GENERAL_OBLIG)); 383 if (actor) { 384 tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.OBLIG_ACT)); 385 } 386 if (elementId) { 387 tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.OBLIG_ELE)); 388 } 389 if (usage) { 390 tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_USAGE)); 391 } 392 if (doco) { 393 tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_DOCUMENTATION)); 394 } 395 if (filter) { 396 tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_FILTER)); 397 } 398 for (ObligationDetail ob : obligations) { 399 tr = new XhtmlNode(NodeType.Element, "tr"); 400 if (ob.unchanged()) { 401 tr.style(STYLE_REMOVED); 402 } else if (ob.removed) { 403 tr.style(STYLE_REMOVED); 404 } 405 children.add(tr); 406 407 XhtmlNode code = tr.td().style("font-size: 11px"); 408 if (ob.compare!=null && ob.getCodes().equals(ob.compare.getCodes())) 409 code.style("font-color: darkgray"); 410 renderCodes(code.getChildNodes(), ob.getCodeList()); 411 if (ob.compare!=null && !ob.compare.getCodeList().isEmpty() && !ob.getCodes().equals(ob.compare.getCodes())) { 412 code.br(); 413 code = code.span(STYLE_UNCHANGED, null); 414 renderCodes(code.getChildNodes(), ob.compare.getCodeList()); 415 } 416 417 XhtmlNode actorId = tr.td().style("font-size: 11px"); 418 if (!ob.actors.isEmpty() || ob.compare.actors.isEmpty()) { 419 boolean firstActor = false; 420 for (CanonicalType anActor : ob.actors) { 421 ActorDefinition ad = context.getContext().fetchResource(ActorDefinition.class, anActor.toString()); 422 boolean existingActor = ob.compare != null && ob.compare.actors.contains(anActor); 423 424 if (!firstActor) { 425 actorId.br(); 426 firstActor = true; 427 } 428 429 if (!existingActor) 430 actorId.style(STYLE_UNCHANGED); 431 432 } 433 434 if (ob.compare != null) { 435 for (CanonicalType compActor : ob.compare.actors) { 436 if (!ob.actors.contains(compActor)) { 437 ActorDefinition compAd = context.getContext().fetchResource(ActorDefinition.class, compActor.toString()); 438 if (!firstActor) { 439 actorId.br(); 440 firstActor = true; 441 } 442 actorId = actorId.span(STYLE_REMOVED, null); 443 if (compAd.hasWebPath()) { 444 actorId.ah(compAd.getWebPath(), compActor.toString()).tx(compAd.present()); 445 } else { 446 actorId.span(null, compActor.toString()).tx(compAd.present()); 447 } 448 } 449 } 450 } 451 } 452 453 454 if (elementId) { 455 XhtmlNode elementIds = tr.td().style("font-size: 11px"); 456 if (ob.compare!=null && ob.elementIds.equals(ob.compare.elementIds)) 457 elementIds.style(STYLE_UNCHANGED); 458 for (String eid : ob.elementIds) { 459 elementIds.sep(", "); 460 ElementDefinition ed = profile.getSnapshot().getElementById(eid); 461 boolean inScope = inScopePaths.contains(ed.getPath()); 462 String name = eid.substring(eid.indexOf(".") + 1); 463 if (ed != null && inScope) { 464 String link = defPath + "#" + anchorPrefix + eid; 465 elementIds.ah(link).tx(name); 466 } else { 467 elementIds.code().tx(name); 468 } 469 } 470 471 if (ob.compare!=null && !ob.compare.elementIds.isEmpty()) { 472 for (String eid : ob.compare.elementIds) { 473 if (!ob.elementIds.contains(eid)) { 474 elementIds.sep(", "); 475 elementIds.span(STYLE_REMOVED, null).code().tx(eid); 476 } 477 } 478 } 479 } 480 if (usage) { 481 if (ob.usage != null) { 482 boolean first = true; 483 XhtmlNode td = tr.td(); 484 for (UsageContext u : ob.usage) { 485 if (first) first = false; else td.tx(", "); 486 new DataRenderer(context).render(td, u); 487 } 488 } else { 489 tr.td(); 490 } 491 } 492 if (doco) { 493 if (ob.doco != null) { 494 String d = fullDoco ? md.processMarkdown("Obligation.documentation", ob.doco) : ob.docoShort; 495 String oldD = ob.compare==null ? null : fullDoco ? md.processMarkdown("Binding.description.compare", ob.compare.doco) : ob.compare.docoShort; 496 tr.td().style("font-size: 11px").innerHTML(compareHtml(d, oldD)); 497 } else { 498 tr.td().style("font-size: 11px"); 499 } 500 } 501 502 if (filter) { 503 if (ob.filter != null) { 504 String d = "<code>"+ob.filter+"</code>" + (fullDoco ? md.processMarkdown("Binding.description", ob.filterDoco) : ""); 505 String oldD = ob.compare==null ? null : "<code>"+ob.compare.filter+"</code>" + (fullDoco ? md.processMarkdown("Binding.description", ob.compare.filterDoco) : ""); 506 tr.td().style("font-size: 11px").innerHTML(compareHtml(d, oldD)); 507 } else { 508 tr.td().style("font-size: 11px"); 509 } 510 } 511 } 512 } 513 514 private XhtmlNode compareString(XhtmlNode node, String newS, String oldS) { 515 if (oldS==null) 516 return node.tx(newS); 517 if (newS.equals(oldS)) 518 return node.style(STYLE_UNCHANGED).tx(newS); 519 node.tx(newS); 520 node.br(); 521 return node.span(STYLE_REMOVED,null).tx(oldS); 522 } 523 524 private String compareHtml(String newS, String oldS) { 525 if (oldS==null) 526 return newS; 527 if (newS.equals(oldS)) 528 return "<span style=\"" + STYLE_UNCHANGED + "\">" + newS + "</span>"; 529 return newS + "<br/><span style=\"" + STYLE_REMOVED + "\">" + oldS + "</span>"; 530 } 531 532 private void renderCodes(XhtmlNodeList children, List<String> codes) { 533 534 if (!codes.isEmpty()) { 535 boolean first = true; 536 for (String code : codes) { 537 if (first) first = false; else children.tx(" & "); 538 int i = code.indexOf(":"); 539 if (i > -1) { 540 String c = code.substring(0, i); 541 code = code.substring(i+1); 542 children.b().tx(c.toUpperCase()); 543 children.tx(":"); 544 } 545 CodeResolution cr = this.cr.resolveCode("http://hl7.org/fhir/tools/CodeSystem/obligation", code); 546 code = code.replace("will-", "").replace("can-", ""); 547 if (cr.getLink() != null) { 548 children.ah(cr.getLink(), cr.getHint()).tx(code); 549 } else { 550 children.span(null, cr.getHint()).tx(code); 551 } 552 } 553 } else { 554 children.span(null, "No Obligation Code?").tx("??"); 555 } 556 } 557 558 public boolean hasObligations() { 559 return !obligations.isEmpty(); 560 } 561 562 private String displayForUsage(Coding c) { 563 if (c.hasDisplay()) { 564 return c.getDisplay(); 565 } 566 if ("http://terminology.hl7.org/CodeSystem/usage-context-type".equals(c.getSystem())) { 567 return c.getCode(); 568 } 569 return c.getCode(); 570 } 571 572}