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