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 UsageContext usage; 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 && ((usage==null && compare.usage==null) || usage.equals(compare.usage)); 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 abr.usage = (ext.hasExtension("usage")) && ext.getExtensionByUrl("usage").hasValueUsageContext() ? ext.getExtensionByUrl("usage").getValueUsageContext() : null; 179 abr.any = "any".equals(ext.getExtensionString("scope")); 180 abr.isUnchanged = ext.hasUserData(ProfileUtilities.UD_DERIVATION_EQUALS); 181 return abr; 182 } 183 184 protected AdditionalBindingDetail additionalBinding(ElementDefinitionBindingAdditionalComponent ab) { 185 AdditionalBindingDetail abr = new AdditionalBindingDetail(); 186 abr.purpose = ab.getPurpose().toCode(); 187 abr.valueSet = ab.getValueSet(); 188 abr.doco = ab.getDocumentation(); 189 abr.docoShort = ab.getShortDoco(); 190 abr.usage = ab.hasUsage() ? ab.getUsageFirstRep() : null; 191 abr.any = ab.getAny(); 192 abr.isUnchanged = ab.hasUserData(ProfileUtilities.UD_DERIVATION_EQUALS); 193 return abr; 194 } 195 196 public String render() throws IOException { 197 if (bindings.isEmpty()) { 198 return ""; 199 } else { 200 XhtmlNode tbl = new XhtmlNode(NodeType.Element, "table"); 201 tbl.attribute("class", "grid"); 202 render(tbl.getChildNodes(), true); 203 return new XhtmlComposer(false).compose(tbl); 204 } 205 } 206 207 public void render(HierarchicalTableGenerator gen, Cell c) throws FHIRFormatError, DefinitionException, IOException { 208 if (bindings.isEmpty()) { 209 return; 210 } else { 211 Piece piece = gen.new Piece("table").attr("class", "grid"); 212 c.getPieces().add(piece); 213 render(piece.getChildren(), false); 214 } 215 } 216 217 public void render(List<XhtmlNode> children, boolean fullDoco) throws FHIRFormatError, DefinitionException, IOException { 218 boolean doco = false; 219 boolean usage = false; 220 boolean any = false; 221 for (AdditionalBindingDetail binding : bindings) { 222 doco = doco || binding.getDoco(fullDoco)!=null || (binding.compare!=null && binding.compare.getDoco(fullDoco)!=null); 223 usage = usage || binding.usage != null || (binding.compare!=null && binding.compare.usage!=null); 224 any = any || binding.any || (binding.compare!=null && binding.compare.any); 225 } 226 227 XhtmlNode tr = new XhtmlNode(NodeType.Element, "tr"); 228 children.add(tr); 229 tr.td().style("font-size: 11px").b().tx(context.formatPhrase(RenderingContext.ADD_BIND_ADD_BIND)); 230 tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_PURPOSE)); 231 if (usage) { 232 tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_USAGE)); 233 } 234 if (any) { 235 tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.ADD_BIND_ANY)); 236 } 237 if (doco) { 238 tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_DOCUMENTATION)); 239 } 240 for (AdditionalBindingDetail binding : bindings) { 241 tr = new XhtmlNode(NodeType.Element, "tr"); 242 if (binding.unchanged()) { 243 tr.style(STYLE_REMOVED); 244 } else if (binding.removed) { 245 tr.style(STYLE_REMOVED); 246 } 247 children.add(tr); 248 BindingResolution br = pkp == null ? makeNullBr(binding) : pkp.resolveBinding(profile, binding.valueSet, path); 249 BindingResolution compBr = null; 250 if (binding.compare!=null && binding.compare.valueSet!=null) 251 compBr = pkp == null ? makeNullBr(binding.compare) : pkp.resolveBinding(profile, binding.compare.valueSet, path); 252 253 XhtmlNode valueset = tr.td().style("font-size: 11px"); 254 if (binding.compare!=null && binding.valueSet.equals(binding.compare.valueSet)) 255 valueset.style(STYLE_UNCHANGED); 256 if (br.url != null) { 257 XhtmlNode a = valueset.ah(context.prefixLocalHref(determineUrl(br.url)), br.uri); 258 a.tx(br.display); 259 if (br.external) { 260 a.tx(" "); 261 a.img("external.png", null); 262 } 263 } else { 264 valueset.span(null, binding.valueSet).tx(br.display); 265 } 266 if (binding.compare!=null && binding.compare.valueSet!=null && !binding.valueSet.equals(binding.compare.valueSet)) { 267 valueset.br(); 268 valueset = valueset.span(STYLE_REMOVED, null); 269 if (compBr.url != null) { 270 valueset.ah(context.prefixLocalHref(determineUrl(compBr.url)), binding.compare.valueSet).tx(compBr.display); 271 } else { 272 valueset.span(null, binding.compare.valueSet).tx(compBr.display); 273 } 274 } 275 276 XhtmlNode purpose = tr.td().style("font-size: 11px"); 277 if (binding.compare!=null && binding.purpose.equals(binding.compare.purpose)) 278 purpose.style("font-color: darkgray"); 279 renderPurpose(purpose, binding.purpose); 280 if (binding.compare!=null && binding.compare.purpose!=null && !binding.purpose.equals(binding.compare.purpose)) { 281 purpose.br(); 282 purpose = purpose.span(STYLE_UNCHANGED, null); 283 renderPurpose(purpose, binding.compare.purpose); 284 } 285 if (usage) { 286 if (binding.usage != null) { 287 // TODO: This isn't rendered at all yet. Ideally, we want it to render with comparison... 288 new DataRenderer(context).renderBase(new RenderingStatus(), tr.td(), binding.usage); 289 } else { 290 tr.td(); 291 } 292 } 293 if (any) { 294 String newRepeat = binding.any ? context.formatPhrase(RenderingContext.ADD_BIND_ANY_REP) : context.formatPhrase(RenderingContext.ADD_BIND_ALL_REP); 295 String oldRepeat = binding.compare!=null && binding.compare.any ? context.formatPhrase(RenderingContext.ADD_BIND_ANY_REP) : context.formatPhrase(RenderingContext.ADD_BIND_ALL_REP); 296 compareString(tr.td().style("font-size: 11px"), newRepeat, oldRepeat); 297 } 298 if (doco) { 299 if (binding.doco != null) { 300 String d = fullDoco ? md.processMarkdown("Binding.description", binding.doco) : binding.docoShort; 301 String oldD = binding.compare==null ? null : fullDoco ? md.processMarkdown("Binding.description.compare", binding.compare.doco) : binding.compare.docoShort; 302 tr.td().style("font-size: 11px").innerHTML(compareHtml(d, oldD)); 303 } else { 304 tr.td().style("font-size: 11px"); 305 } 306 } 307 } 308 } 309 310 private XhtmlNode compareString(XhtmlNode node, String newS, String oldS) { 311 if (oldS==null) 312 return node.tx(newS); 313 if (newS.equals(oldS)) 314 return node.style(STYLE_UNCHANGED).tx(newS); 315 node.tx(newS); 316 node.br(); 317 return node.span(STYLE_REMOVED,null).tx(oldS); 318 } 319 320 private String compareHtml(String newS, String oldS) { 321 if (oldS==null) 322 return newS; 323 if (newS.equals(oldS)) 324 return "<span style=\"" + STYLE_UNCHANGED + "\">" + newS + "</span>"; 325 return newS + "<br/><span style=\"" + STYLE_REMOVED + "\">" + oldS + "</span>"; 326 } 327 328 private String determineUrl(String url) { 329 return Utilities.isAbsoluteUrl(url) || !pkp.prependLinks() ? url : corePath + url; 330 } 331 332 private void renderPurpose(XhtmlNode td, String purpose) { 333 boolean r5 = context == null || context.getWorker() == null ? false : VersionUtilities.isR5Plus(context.getWorker().getVersion()); 334 switch (purpose) { 335 case "maximum": 336 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)); 337 break; 338 case "minimum": 339 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)); 340 break; 341 case "required" : 342 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)); 343 break; 344 case "extensible" : 345 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)); 346 break; 347 case "preferred" : 348 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)); 349 break; 350 case "current" : 351 if (r5) { 352 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)); 353 } else { 354 td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_NEW_REC)).tx(context.formatPhrase(RenderingContext.GENERAL_REQUIRED)); 355 } 356 break; 357 case "ui" : 358 if (r5) { 359 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)); 360 } else { 361 td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_GIVEN_CONT)).tx(context.formatPhrase(RenderingContext.ADD_BIND_UI)); 362 } 363 break; 364 case "starter" : 365 if (r5) { 366 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"); 367 } else { 368 td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_DESIG_SYS)).tx(context.formatPhrase(RenderingContext.GENERAL_STARTER)); 369 } 370 break; 371 case "component" : 372 if (r5) { 373 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"); 374 } else { 375 td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_VALUE_COMP)).tx(context.formatPhrase(RenderingContext.GENERAL_COMPONENT)); 376 } 377 break; 378 default: 379 td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_UNKNOWN_PUR)).tx(purpose); 380 } 381 } 382 383 private BindingResolution makeNullBr(AdditionalBindingDetail binding) { 384 BindingResolution br = new BindingResolution(); 385 br.url = "http://none.none/none"; 386 br.display = "todo"; 387 return br; 388 } 389 390 public boolean hasBindings() { 391 return !bindings.isEmpty(); 392 } 393 394 public void render(XhtmlNodeList children, List<ElementDefinitionBindingAdditionalComponent> list) { 395 if (list.size() == 1) { 396 render(children, list.get(0)); 397 } else { 398 XhtmlNode ul = children.ul(); 399 for (ElementDefinitionBindingAdditionalComponent b : list) { 400 render(ul.li().getChildNodes(), b); 401 } 402 } 403 } 404 405 private void render(XhtmlNodeList children, ElementDefinitionBindingAdditionalComponent b) { 406 if (b.getValueSet() == null) { 407 return; // what should happen? 408 } 409 BindingResolution br = pkp.resolveBinding(profile, b.getValueSet(), corePath); 410 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); 411 if (b.hasDocumentation()) { 412 a.attribute("title", b.getDocumentation()); 413 } 414 a.tx(br.display); 415 416 if (b.hasShortDoco()) { 417 children.tx(": "); 418 children.tx(b.getShortDoco()); 419 } 420 if (b.getAny() || b.hasUsage()) { 421 children.tx(" ("); 422 boolean ffirst = !b.getAny(); 423 if (b.getAny()) { 424 children.tx(context.formatPhrase(RenderingContext.ADD_BIND_ANY_REP)); 425 } 426 for (UsageContext uc : b.getUsage()) { 427 if (ffirst) ffirst = false; else children.tx(","); 428 if (!uc.getCode().is("http://terminology.hl7.org/CodeSystem/usage-context-type", "jurisdiction")) { 429 children.tx(displayForUsage(uc.getCode())); 430 children.tx("="); 431 } 432 CodeResolution ccr = cr.resolveCode(uc.getValueCodeableConcept()); 433 children.ah(context.prefixLocalHref(ccr.getLink()), ccr.getHint()).tx(ccr.getDisplay()); 434 } 435 children.tx(")"); 436 } 437 } 438 439 440 private String displayForUsage(Coding c) { 441 if (c.hasDisplay()) { 442 return c.getDisplay(); 443 } 444 if ("http://terminology.hl7.org/CodeSystem/usage-context-type".equals(c.getSystem())) { 445 return c.getCode(); 446 } 447 return c.getCode(); 448 } 449 450 public void seeAdditionalBinding(String purpose, String doco, ValueSet valueSet) { 451 AdditionalBindingDetail abr = new AdditionalBindingDetail(); 452 abr.purpose = purpose; 453 abr.valueSet = valueSet.getUrl(); 454 abr.vs = valueSet; 455 bindings.add(abr); 456 } 457 458 public void seeAdditionalBinding(String purpose, String doco, String ref) { 459 AdditionalBindingDetail abr = new AdditionalBindingDetail(); 460 abr.purpose = purpose; 461 abr.valueSet = ref; 462 bindings.add(abr); 463 464 } 465 466 public void seeAdditionalBindings(ElementDefinition definition, ElementDefinition compDef, boolean compare) { 467 HashMap<String, AdditionalBindingDetail> compBindings = new HashMap<String, AdditionalBindingDetail>(); 468 if (compare && compDef.getBinding().getAdditional() != null) { 469 for (ElementDefinitionBindingAdditionalComponent ab : compDef.getBinding().getAdditional()) { 470 AdditionalBindingDetail abr = additionalBinding(ab); 471 if (compBindings.containsKey(abr.getKey())) { 472 abr.incrementCount(); 473 } 474 compBindings.put(abr.getKey(), abr); 475 } 476 } 477 478 for (ElementDefinitionBindingAdditionalComponent ab : definition.getBinding().getAdditional()) { 479 AdditionalBindingDetail abr = additionalBinding(ab); 480 if (compare && compDef != null) { 481 AdditionalBindingDetail match = null; 482 do { 483 match = compBindings.get(abr.getKey()); 484 if (abr.alreadyMatched()) 485 abr.incrementCount(); 486 } while (match!=null && abr.alreadyMatched()); 487 if (match!=null) 488 abr.setCompare(match); 489 bindings.add(abr); 490 if (abr.compare!=null) 491 compBindings.remove(abr.compare.getKey()); 492 } else 493 bindings.add(abr); 494 } 495 for (AdditionalBindingDetail b: compBindings.values()) { 496 b.removed = true; 497 bindings.add(b); 498 } 499 500 } 501 502}