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