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