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