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.Coding; 017import org.hl7.fhir.r5.model.ElementDefinition; 018import org.hl7.fhir.r5.model.Extension; 019import org.hl7.fhir.r5.model.PrimitiveType; 020import org.hl7.fhir.r5.model.StructureDefinition; 021import org.hl7.fhir.r5.model.UsageContext; 022import org.hl7.fhir.r5.model.ValueSet; 023import org.hl7.fhir.r5.renderers.CodeResolver.CodeResolution; 024import org.hl7.fhir.r5.renderers.utils.RenderingContext; 025import org.hl7.fhir.r5.utils.PublicationHacker; 026import org.hl7.fhir.r5.utils.ToolingExtensions; 027import org.hl7.fhir.utilities.MarkDownProcessor; 028import org.hl7.fhir.utilities.Utilities; 029import org.hl7.fhir.utilities.VersionUtilities; 030import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator; 031import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Cell; 032import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Piece; 033import org.hl7.fhir.utilities.xhtml.NodeType; 034import org.hl7.fhir.utilities.xhtml.XhtmlComposer; 035import org.hl7.fhir.utilities.xhtml.XhtmlNode; 036import org.hl7.fhir.utilities.xhtml.XhtmlNodeList; 037 038public class AdditionalBindingsRenderer { 039 public class AdditionalBindingDetail { 040 private String purpose; 041 private String valueSet; 042 private String doco; 043 private String docoShort; 044 private UsageContext usage; 045 private boolean any = false; 046 private boolean isUnchanged = false; 047 private boolean matched = false; 048 private boolean removed = false; 049 private ValueSet vs; 050 051 private AdditionalBindingDetail compare; 052 private int count = 1; 053 private String getKey() { 054 // Todo: Consider extending this with content from usageContext if purpose isn't sufficiently differentiating 055 return purpose + Integer.toString(count); 056 } 057 private void incrementCount() { 058 count++; 059 } 060 private void setCompare(AdditionalBindingDetail match) { 061 compare = match; 062 match.matched = true; 063 } 064 private boolean alreadyMatched() { 065 return matched; 066 } 067 public String getDoco(boolean full) { 068 return full ? doco : docoShort; 069 } 070 public boolean unchanged() { 071 if (!isUnchanged) 072 return false; 073 if (compare==null) 074 return true; 075 isUnchanged = true; 076 isUnchanged = isUnchanged && ((purpose==null && compare.purpose==null) || purpose.equals(compare.purpose)); 077 isUnchanged = isUnchanged && ((valueSet==null && compare.valueSet==null) || valueSet.equals(compare.valueSet)); 078 isUnchanged = isUnchanged && ((doco==null && compare.doco==null) || doco.equals(compare.doco)); 079 isUnchanged = isUnchanged && ((docoShort==null && compare.docoShort==null) || docoShort.equals(compare.docoShort)); 080 isUnchanged = isUnchanged && ((usage==null && compare.usage==null) || usage.equals(compare.usage)); 081 return isUnchanged; 082 } 083 } 084 085 private static String STYLE_UNCHANGED = "opacity: 0.5;"; 086 private static String STYLE_REMOVED = STYLE_UNCHANGED + "text-decoration: line-through;"; 087 088 private List<AdditionalBindingDetail> bindings = new ArrayList<>(); 089 private ProfileKnowledgeProvider pkp; 090 private String corePath; 091 private StructureDefinition profile; 092 private String path; 093 private RenderingContext context; 094 private IMarkdownProcessor md; 095 private CodeResolver cr; 096 097 public AdditionalBindingsRenderer(ProfileKnowledgeProvider pkp, String corePath, StructureDefinition profile, String path, RenderingContext context, IMarkdownProcessor md, CodeResolver cr) { 098 this.pkp = pkp; 099 this.corePath = corePath; 100 this.profile = profile; 101 this.path = path; 102 this.context = context; 103 this.md = md; 104 this.cr = cr; 105 } 106 107 public void seeMaxBinding(Extension ext) { 108 seeMaxBinding(ext, null, false); 109 } 110 111 public void seeMaxBinding(Extension ext, Extension compExt, boolean compare) { 112 seeBinding(ext, compExt, compare, "maximum"); 113 } 114 115 protected void seeBinding(Extension ext, Extension compExt, boolean compare, String label) { 116 AdditionalBindingDetail abr = new AdditionalBindingDetail(); 117 abr.purpose = label; 118 abr.valueSet = ext.getValue().primitiveValue(); 119 if (compare) { 120 abr.isUnchanged = compExt!=null && ext.getValue().primitiveValue().equals(compExt.getValue().primitiveValue()); 121 122 abr.compare = new AdditionalBindingDetail(); 123 abr.compare.valueSet = compExt==null ? null : compExt.getValue().primitiveValue(); 124 } else { 125 abr.isUnchanged = ext.hasUserData(ProfileUtilities.UD_DERIVATION_EQUALS); 126 } 127 bindings.add(abr); 128 } 129 130 public void seeMinBinding(Extension ext) { 131 seeMinBinding(ext, null, false); 132 } 133 134 public void seeMinBinding(Extension ext, Extension compExt, boolean compare) { 135 seeBinding(ext, compExt, compare, "minimum"); 136 } 137 138 public void seeAdditionalBindings(List<Extension> list) { 139 seeAdditionalBindings(list, null, false); 140 } 141 142 public void seeAdditionalBindings(List<Extension> list, List<Extension> compList, boolean compare) { 143 HashMap<String, AdditionalBindingDetail> compBindings = new HashMap<String, AdditionalBindingDetail>(); 144 if (compare && compList!=null) { 145 for (Extension ext : compList) { 146 AdditionalBindingDetail abr = additionalBinding(ext); 147 if (compBindings.containsKey(abr.getKey())) { 148 abr.incrementCount(); 149 } 150 compBindings.put(abr.getKey(), abr); 151 } 152 } 153 154 for (Extension ext : list) { 155 AdditionalBindingDetail abr = additionalBinding(ext); 156 if (compare && compList!=null) { 157 AdditionalBindingDetail match = null; 158 do { 159 match = compBindings.get(abr.getKey()); 160 if (abr.alreadyMatched()) 161 abr.incrementCount(); 162 } while (match!=null && abr.alreadyMatched()); 163 if (match!=null) 164 abr.setCompare(match); 165 bindings.add(abr); 166 if (abr.compare!=null) 167 compBindings.remove(abr.compare.getKey()); 168 } else 169 bindings.add(abr); 170 } 171 for (AdditionalBindingDetail b: compBindings.values()) { 172 b.removed = true; 173 bindings.add(b); 174 } 175 } 176 177 protected AdditionalBindingDetail additionalBinding(Extension ext) { 178 AdditionalBindingDetail abr = new AdditionalBindingDetail(); 179 abr.purpose = ext.getExtensionString("purpose"); 180 abr.valueSet = ext.getExtensionString("valueSet"); 181 abr.doco = ext.getExtensionString("documentation"); 182 abr.docoShort = ext.getExtensionString("shortDoco"); 183 abr.usage = (ext.hasExtension("usage")) && ext.getExtensionByUrl("usage").hasValueUsageContext() ? ext.getExtensionByUrl("usage").getValueUsageContext() : null; 184 abr.any = "any".equals(ext.getExtensionString("scope")); 185 abr.isUnchanged = ext.hasUserData(ProfileUtilities.UD_DERIVATION_EQUALS); 186 return abr; 187 } 188 189 protected AdditionalBindingDetail additionalBinding(ElementDefinitionBindingAdditionalComponent ab) { 190 AdditionalBindingDetail abr = new AdditionalBindingDetail(); 191 abr.purpose = ab.getPurpose().toCode(); 192 abr.valueSet = ab.getValueSet(); 193 abr.doco = ab.getDocumentation(); 194 abr.docoShort = ab.getShortDoco(); 195 abr.usage = ab.hasUsage() ? ab.getUsageFirstRep() : null; 196 abr.any = ab.getAny(); 197 abr.isUnchanged = ab.hasUserData(ProfileUtilities.UD_DERIVATION_EQUALS); 198 return abr; 199 } 200 201 public String render() throws IOException { 202 if (bindings.isEmpty()) { 203 return ""; 204 } else { 205 XhtmlNode tbl = new XhtmlNode(NodeType.Element, "table"); 206 tbl.attribute("class", "grid"); 207 render(tbl.getChildNodes(), true); 208 return new XhtmlComposer(false).compose(tbl); 209 } 210 } 211 212 public void render(HierarchicalTableGenerator gen, Cell c) throws FHIRFormatError, DefinitionException, IOException { 213 if (bindings.isEmpty()) { 214 return; 215 } else { 216 Piece piece = gen.new Piece("table").attr("class", "grid"); 217 c.getPieces().add(piece); 218 render(piece.getChildren(), false); 219 } 220 } 221 222 public void render(List<XhtmlNode> children, boolean fullDoco) throws FHIRFormatError, DefinitionException, IOException { 223 boolean doco = false; 224 boolean usage = false; 225 boolean any = false; 226 for (AdditionalBindingDetail binding : bindings) { 227 doco = doco || binding.getDoco(fullDoco)!=null || (binding.compare!=null && binding.compare.getDoco(fullDoco)!=null); 228 usage = usage || binding.usage != null || (binding.compare!=null && binding.compare.usage!=null); 229 any = any || binding.any || (binding.compare!=null && binding.compare.any); 230 } 231 232 XhtmlNode tr = new XhtmlNode(NodeType.Element, "tr"); 233 children.add(tr); 234 tr.td().style("font-size: 11px").b().tx(context.formatPhrase(RenderingContext.ADD_BIND_ADD_BIND)); 235 tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_PURPOSE)); 236 if (usage) { 237 tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_USAGE)); 238 } 239 if (any) { 240 tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.ADD_BIND_ANY)); 241 } 242 if (doco) { 243 tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_DOCUMENTATION)); 244 } 245 for (AdditionalBindingDetail binding : bindings) { 246 tr = new XhtmlNode(NodeType.Element, "tr"); 247 if (binding.unchanged()) { 248 tr.style(STYLE_REMOVED); 249 } else if (binding.removed) { 250 tr.style(STYLE_REMOVED); 251 } 252 children.add(tr); 253 BindingResolution br = pkp == null ? makeNullBr(binding) : pkp.resolveBinding(profile, binding.valueSet, path); 254 BindingResolution compBr = null; 255 if (binding.compare!=null && binding.compare.valueSet!=null) 256 compBr = pkp == null ? makeNullBr(binding.compare) : pkp.resolveBinding(profile, binding.compare.valueSet, path); 257 258 XhtmlNode valueset = tr.td().style("font-size: 11px"); 259 if (binding.compare!=null && binding.valueSet.equals(binding.compare.valueSet)) 260 valueset.style(STYLE_UNCHANGED); 261 if (br.url != null) { 262 XhtmlNode a = valueset.ah(determineUrl(br.url), br.uri); 263 a.tx(br.display); 264 if (br.external) { 265 a.tx(" "); 266 a.img("external.png", null); 267 } 268 } else { 269 valueset.span(null, binding.valueSet).tx(br.display); 270 } 271 if (binding.compare!=null && binding.compare.valueSet!=null && !binding.valueSet.equals(binding.compare.valueSet)) { 272 valueset.br(); 273 valueset = valueset.span(STYLE_REMOVED, null); 274 if (compBr.url != null) { 275 valueset.ah(determineUrl(compBr.url), binding.compare.valueSet).tx(compBr.display); 276 } else { 277 valueset.span(null, binding.compare.valueSet).tx(compBr.display); 278 } 279 } 280 281 XhtmlNode purpose = tr.td().style("font-size: 11px"); 282 if (binding.compare!=null && binding.purpose.equals(binding.compare.purpose)) 283 purpose.style("font-color: darkgray"); 284 renderPurpose(purpose, binding.purpose); 285 if (binding.compare!=null && binding.compare.purpose!=null && !binding.purpose.equals(binding.compare.purpose)) { 286 purpose.br(); 287 purpose = purpose.span(STYLE_UNCHANGED, null); 288 renderPurpose(purpose, binding.compare.purpose); 289 } 290 if (usage) { 291 if (binding.usage != null) { 292 // TODO: This isn't rendered at all yet. Ideally, we want it to render with comparison... 293 new DataRenderer(context).render(tr.td(), binding.usage); 294 } else { 295 tr.td(); 296 } 297 } 298 if (any) { 299 String newRepeat = binding.any ? context.formatPhrase(RenderingContext.ADD_BIND_ANY_REP) : context.formatPhrase(RenderingContext.ADD_BIND_ALL_REP); 300 String oldRepeat = binding.compare!=null && binding.compare.any ? context.formatPhrase(RenderingContext.ADD_BIND_ANY_REP) : context.formatPhrase(RenderingContext.ADD_BIND_ALL_REP); 301 compareString(tr.td().style("font-size: 11px"), newRepeat, oldRepeat); 302 } 303 if (doco) { 304 if (binding.doco != null) { 305 String d = fullDoco ? md.processMarkdown("Binding.description", binding.doco) : binding.docoShort; 306 String oldD = binding.compare==null ? null : fullDoco ? md.processMarkdown("Binding.description.compare", binding.compare.doco) : binding.compare.docoShort; 307 tr.td().style("font-size: 11px").innerHTML(compareHtml(d, oldD)); 308 } else { 309 tr.td().style("font-size: 11px"); 310 } 311 } 312 } 313 } 314 315 private XhtmlNode compareString(XhtmlNode node, String newS, String oldS) { 316 if (oldS==null) 317 return node.tx(newS); 318 if (newS.equals(oldS)) 319 return node.style(STYLE_UNCHANGED).tx(newS); 320 node.tx(newS); 321 node.br(); 322 return node.span(STYLE_REMOVED,null).tx(oldS); 323 } 324 325 private String compareHtml(String newS, String oldS) { 326 if (oldS==null) 327 return newS; 328 if (newS.equals(oldS)) 329 return "<span style=\"" + STYLE_UNCHANGED + "\">" + newS + "</span>"; 330 return newS + "<br/><span style=\"" + STYLE_REMOVED + "\">" + oldS + "</span>"; 331 } 332 333 private String determineUrl(String url) { 334 return Utilities.isAbsoluteUrl(url) || !pkp.prependLinks() ? url : corePath + url; 335 } 336 337 private void renderPurpose(XhtmlNode td, String purpose) { 338 boolean r5 = context == null || context.getWorker() == null ? false : VersionUtilities.isR5Plus(context.getWorker().getVersion()); 339 switch (purpose) { 340 case "maximum": 341 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-maximum" : corePath+"extension-elementdefinition-maxvalueset.html", context.formatPhrase(RenderingContext.ADD_BIND_EXT_PREF)).tx(context.formatPhrase(RenderingContext.ADD_BIND_MAX)); 342 break; 343 case "minimum": 344 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-minimum" : corePath+"extension-elementdefinition-minvalueset.html", context.formatPhrase(RenderingContext.GENERAL_BIND_MIN_ALLOW)).tx(context.formatPhrase(RenderingContext.ADD_BIND_MIN)); 345 break; 346 case "required" : 347 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-required" : corePath+"terminologies.html#strength", context.formatPhrase(RenderingContext.ADD_BIND_VALID_REQ)).tx(context.formatPhrase(RenderingContext.ADD_BIND_REQ_BIND)); 348 break; 349 case "extensible" : 350 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-extensible" : corePath+"terminologies.html#strength", context.formatPhrase(RenderingContext.ADD_BIND_VALID_EXT)).tx(context.formatPhrase(RenderingContext.ADD_BIND_EX_BIND)); 351 break; 352 case "current" : 353 if (r5) { 354 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-current" : corePath+"terminologies.html#strength", context.formatPhrase(RenderingContext.ADD_BIND_NEW_REC)).tx(context.formatPhrase(RenderingContext.ADD_BIND_CURR_BIND)); 355 } else { 356 td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_NEW_REC)).tx(context.formatPhrase(RenderingContext.GENERAL_REQUIRED)); 357 } 358 break; 359 case "preferred" : 360 if (r5) { 361 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-preferred" : corePath+"terminologies.html#strength", context.formatPhrase(RenderingContext.ADD_BIND_RECOM_VALUE_SET)).tx(context.formatPhrase(RenderingContext.ADD_BIND_PREF_BIND)); 362 } else { 363 td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_RECOM_VALUE_SET)).tx(context.formatPhrase(RenderingContext.GENERAL_PREFERRED)); 364 } 365 break; 366 case "ui" : 367 if (r5) { 368 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-ui" : corePath+"terminologies.html#strength", context.formatPhrase(RenderingContext.ADD_BIND_GIVEN_CONT)).tx(context.formatPhrase(RenderingContext.ADD_BIND_UI_BIND)); 369 } else { 370 td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_GIVEN_CONT)).tx(context.formatPhrase(RenderingContext.ADD_BIND_UI)); 371 } 372 break; 373 case "starter" : 374 if (r5) { 375 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-starter" : corePath+"terminologies.html#strength", "This value set is a good set of codes to start with when designing your system").tx("Starter Set"); 376 } else { 377 td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_DESIG_SYS)).tx(context.formatPhrase(RenderingContext.GENERAL_STARTER)); 378 } 379 break; 380 case "component" : 381 if (r5) { 382 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-component" : corePath+"terminologies.html#strength", "This value set is a component of the base value set").tx("Component"); 383 } else { 384 td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_VALUE_COMP)).tx(context.formatPhrase(RenderingContext.GENERAL_COMPONENT)); 385 } 386 break; 387 default: 388 td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_UNKNOWN_PUR)).tx(purpose); 389 } 390 } 391 392 private BindingResolution makeNullBr(AdditionalBindingDetail binding) { 393 BindingResolution br = new BindingResolution(); 394 br.url = "http://none.none/none"; 395 br.display = "todo"; 396 return br; 397 } 398 399 public boolean hasBindings() { 400 return !bindings.isEmpty(); 401 } 402 403 public void render(XhtmlNodeList children, List<ElementDefinitionBindingAdditionalComponent> list) { 404 if (list.size() == 1) { 405 render(children, list.get(0)); 406 } else { 407 XhtmlNode ul = children.ul(); 408 for (ElementDefinitionBindingAdditionalComponent b : list) { 409 render(ul.li().getChildNodes(), b); 410 } 411 } 412 } 413 414 private void render(XhtmlNodeList children, ElementDefinitionBindingAdditionalComponent b) { 415 if (b.getValueSet() == null) { 416 return; // what should happen? 417 } 418 BindingResolution br = pkp.resolveBinding(profile, b.getValueSet(), corePath); 419 XhtmlNode a = children.ahOrCode(br.url == null ? null : Utilities.isAbsoluteUrl(br.url) || !context.getPkp().prependLinks() ? br.url : corePath+br.url, b.hasDocumentation() ? b.getDocumentation() : br.uri); 420 if (b.hasDocumentation()) { 421 a.attribute("title", b.getDocumentation()); 422 } 423 a.tx(br.display); 424 425 if (b.hasShortDoco()) { 426 children.tx(": "); 427 children.tx(b.getShortDoco()); 428 } 429 if (b.getAny() || b.hasUsage()) { 430 children.tx(" ("); 431 boolean ffirst = !b.getAny(); 432 if (b.getAny()) { 433 children.tx(context.formatPhrase(RenderingContext.ADD_BIND_ANY_REP)); 434 } 435 for (UsageContext uc : b.getUsage()) { 436 if (ffirst) ffirst = false; else children.tx(","); 437 if (!uc.getCode().is("http://terminology.hl7.org/CodeSystem/usage-context-type", "jurisdiction")) { 438 children.tx(displayForUsage(uc.getCode())); 439 children.tx("="); 440 } 441 CodeResolution ccr = cr.resolveCode(uc.getValueCodeableConcept()); 442 children.ah(ccr.getLink(), ccr.getHint()).tx(ccr.getDisplay()); 443 } 444 children.tx(")"); 445 } 446 } 447 448 449 private String displayForUsage(Coding c) { 450 if (c.hasDisplay()) { 451 return c.getDisplay(); 452 } 453 if ("http://terminology.hl7.org/CodeSystem/usage-context-type".equals(c.getSystem())) { 454 return c.getCode(); 455 } 456 return c.getCode(); 457 } 458 459 public void seeAdditionalBinding(String purpose, String doco, ValueSet valueSet) { 460 AdditionalBindingDetail abr = new AdditionalBindingDetail(); 461 abr.purpose = purpose; 462 abr.valueSet = valueSet.getUrl(); 463 abr.vs = valueSet; 464 bindings.add(abr); 465 } 466 467 public void seeAdditionalBinding(String purpose, String doco, String ref) { 468 AdditionalBindingDetail abr = new AdditionalBindingDetail(); 469 abr.purpose = purpose; 470 abr.valueSet = ref; 471 bindings.add(abr); 472 473 } 474 475 public void seeAdditionalBindings(ElementDefinition definition, ElementDefinition compDef, boolean compare) { 476 HashMap<String, AdditionalBindingDetail> compBindings = new HashMap<String, AdditionalBindingDetail>(); 477 if (compare && compDef.getBinding().getAdditional() != null) { 478 for (ElementDefinitionBindingAdditionalComponent ab : compDef.getBinding().getAdditional()) { 479 AdditionalBindingDetail abr = additionalBinding(ab); 480 if (compBindings.containsKey(abr.getKey())) { 481 abr.incrementCount(); 482 } 483 compBindings.put(abr.getKey(), abr); 484 } 485 } 486 487 for (ElementDefinitionBindingAdditionalComponent ab : definition.getBinding().getAdditional()) { 488 AdditionalBindingDetail abr = additionalBinding(ab); 489 if (compare && compDef != null) { 490 AdditionalBindingDetail match = null; 491 do { 492 match = compBindings.get(abr.getKey()); 493 if (abr.alreadyMatched()) 494 abr.incrementCount(); 495 } while (match!=null && abr.alreadyMatched()); 496 if (match!=null) 497 abr.setCompare(match); 498 bindings.add(abr); 499 if (abr.compare!=null) 500 compBindings.remove(abr.compare.getKey()); 501 } else 502 bindings.add(abr); 503 } 504 for (AdditionalBindingDetail b: compBindings.values()) { 505 b.removed = true; 506 bindings.add(b); 507 } 508 509 } 510 511}