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