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 public String render() throws IOException { 190 if (bindings.isEmpty()) { 191 return ""; 192 } else { 193 XhtmlNode tbl = new XhtmlNode(NodeType.Element, "table"); 194 tbl.attribute("class", "grid"); 195 render(tbl.getChildNodes(), true); 196 return new XhtmlComposer(false).compose(tbl); 197 } 198 } 199 200 public void render(HierarchicalTableGenerator gen, Cell c) throws FHIRFormatError, DefinitionException, IOException { 201 if (bindings.isEmpty()) { 202 return; 203 } else { 204 Piece piece = gen.new Piece("table").attr("class", "grid"); 205 c.getPieces().add(piece); 206 render(piece.getChildren(), false); 207 } 208 } 209 210 public void render(List<XhtmlNode> children, boolean fullDoco) throws FHIRFormatError, DefinitionException, IOException { 211 boolean doco = false; 212 boolean usage = false; 213 boolean any = false; 214 for (AdditionalBindingDetail binding : bindings) { 215 doco = doco || binding.getDoco(fullDoco)!=null || (binding.compare!=null && binding.compare.getDoco(fullDoco)!=null); 216 usage = usage || binding.usage != null || (binding.compare!=null && binding.compare.usage!=null); 217 any = any || binding.any || (binding.compare!=null && binding.compare.any); 218 } 219 220 XhtmlNode tr = new XhtmlNode(NodeType.Element, "tr"); 221 children.add(tr); 222 tr.td().style("font-size: 11px").b().tx("Additional Bindings"); 223 tr.td().style("font-size: 11px").tx("Purpose"); 224 if (usage) { 225 tr.td().style("font-size: 11px").tx("Usage"); 226 } 227 if (any) { 228 tr.td().style("font-size: 11px").tx("Any"); 229 } 230 if (doco) { 231 tr.td().style("font-size: 11px").tx("Documentation"); 232 } 233 for (AdditionalBindingDetail binding : bindings) { 234 tr = new XhtmlNode(NodeType.Element, "tr"); 235 if (binding.unchanged()) { 236 tr.style(STYLE_REMOVED); 237 } else if (binding.removed) { 238 tr.style(STYLE_REMOVED); 239 } 240 children.add(tr); 241 BindingResolution br = pkp == null ? makeNullBr(binding) : pkp.resolveBinding(profile, binding.valueSet, path); 242 BindingResolution compBr = null; 243 if (binding.compare!=null && binding.compare.valueSet!=null) 244 compBr = pkp == null ? makeNullBr(binding.compare) : pkp.resolveBinding(profile, binding.compare.valueSet, path); 245 246 XhtmlNode valueset = tr.td().style("font-size: 11px"); 247 if (binding.compare!=null && binding.valueSet.equals(binding.compare.valueSet)) 248 valueset.style(STYLE_UNCHANGED); 249 if (br.url != null) { 250 valueset.ah(determineUrl(br.url), binding.valueSet).tx(br.display); 251 } else { 252 valueset.span(null, binding.valueSet).tx(br.display); 253 } 254 if (binding.compare!=null && binding.compare.valueSet!=null && !binding.valueSet.equals(binding.compare.valueSet)) { 255 valueset.br(); 256 valueset = valueset.span(STYLE_REMOVED, null); 257 if (compBr.url != null) { 258 valueset.ah(determineUrl(compBr.url), binding.compare.valueSet).tx(compBr.display); 259 } else { 260 valueset.span(null, binding.compare.valueSet).tx(compBr.display); 261 } 262 } 263 264 XhtmlNode purpose = tr.td().style("font-size: 11px"); 265 if (binding.compare!=null && binding.purpose.equals(binding.compare.purpose)) 266 purpose.style("font-color: darkgray"); 267 renderPurpose(purpose, binding.purpose); 268 if (binding.compare!=null && binding.compare.purpose!=null && !binding.purpose.equals(binding.compare.purpose)) { 269 purpose.br(); 270 purpose = purpose.span(STYLE_UNCHANGED, null); 271 renderPurpose(purpose, binding.compare.purpose); 272 } 273 if (usage) { 274 if (binding.usage != null) { 275 // TODO: This isn't rendered at all yet. Ideally, we want it to render with comparison... 276 new DataRenderer(context).render(tr.td(), binding.usage); 277 } else { 278 tr.td(); 279 } 280 } 281 if (any) { 282 String newRepeat = binding.any ? "Any repeats" : "All repeats"; 283 String oldRepeat = binding.compare!=null && binding.compare.any ? "Any repeats" : "All repeats"; 284 compareString(tr.td().style("font-size: 11px"), newRepeat, oldRepeat); 285 } 286 if (doco) { 287 if (binding.doco != null) { 288 String d = fullDoco ? md.processMarkdown("Binding.description", binding.doco) : binding.docoShort; 289 String oldD = binding.compare==null ? null : fullDoco ? md.processMarkdown("Binding.description.compare", binding.compare.doco) : binding.compare.docoShort; 290 tr.td().style("font-size: 11px").innerHTML(compareHtml(d, oldD)); 291 } else { 292 tr.td().style("font-size: 11px"); 293 } 294 } 295 } 296 } 297 298 private XhtmlNode compareString(XhtmlNode node, String newS, String oldS) { 299 if (oldS==null) 300 return node.tx(newS); 301 if (newS.equals(oldS)) 302 return node.style(STYLE_UNCHANGED).tx(newS); 303 node.tx(newS); 304 node.br(); 305 return node.span(STYLE_REMOVED,null).tx(oldS); 306 } 307 308 private String compareHtml(String newS, String oldS) { 309 if (oldS==null) 310 return newS; 311 if (newS.equals(oldS)) 312 return "<span style=\"" + STYLE_UNCHANGED + "\">" + newS + "</span>"; 313 return newS + "<br/><span style=\"" + STYLE_REMOVED + "\">" + oldS + "</span>"; 314 } 315 316 private String determineUrl(String url) { 317 return Utilities.isAbsoluteUrl(url) || !pkp.prependLinks() ? url : corePath + url; 318 } 319 320 private void renderPurpose(XhtmlNode td, String purpose) { 321 boolean r5 = context == null || context.getWorker() == null ? false : VersionUtilities.isR5Plus(context.getWorker().getVersion()); 322 switch (purpose) { 323 case "maximum": 324 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-maximum" : corePath+"extension-elementdefinition-maxvalueset.html", "A required binding, for use when the binding strength is 'extensible' or 'preferred'").tx("Max Binding"); 325 break; 326 case "minimum": 327 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-minimum" : corePath+"extension-elementdefinition-minvalueset.html", "The minimum allowable value set - any conformant system SHALL support all these codes").tx("Min Binding"); 328 break; 329 case "required" : 330 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-required" : corePath+"terminologies.html#strength", "Validators will check this binding (strength = required)").tx("Required Binding"); 331 break; 332 case "extensible" : 333 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-extensible" : corePath+"terminologies.html#strength", "Validators will check this binding (strength = extensible)").tx("Extensible Binding"); 334 break; 335 case "current" : 336 if (r5) { 337 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-current" : corePath+"terminologies.html#strength", "New records are required to use this value set, but legacy records may use other codes").tx("Current Binding"); 338 } else { 339 td.span(null, "New records are required to use this value set, but legacy records may use other codes").tx("Required"); 340 } 341 break; 342 case "preferred" : 343 if (r5) { 344 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-preferred" : corePath+"terminologies.html#strength", "This is the value set that is recommended (documentation should explain why)").tx("Preferred Binding"); 345 } else { 346 td.span(null, "This is the value set that is recommended (documentation should explain why)").tx("Recommended"); 347 } 348 break; 349 case "ui" : 350 if (r5) { 351 td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-ui" : corePath+"terminologies.html#strength", "This value set is provided to user look up in a given context").tx("UI Binding"); 352 } else { 353 td.span(null, "This value set is provided to user look up in a given context").tx("UI"); 354 } 355 break; 356 case "starter" : 357 if (r5) { 358 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"); 359 } else { 360 td.span(null, "This value set is a good set of codes to start with when designing your system").tx("Starter"); 361 } 362 break; 363 case "component" : 364 if (r5) { 365 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"); 366 } else { 367 td.span(null, "This value set is a component of the base value set").tx("Component"); 368 } 369 break; 370 default: 371 td.span(null, "Unknown code for purpose").tx(purpose); 372 } 373 } 374 375 private BindingResolution makeNullBr(AdditionalBindingDetail binding) { 376 BindingResolution br = new BindingResolution(); 377 br.url = "http://none.none/none"; 378 br.display = "todo"; 379 return br; 380 } 381 382 public boolean hasBindings() { 383 return !bindings.isEmpty(); 384 } 385 386 public void render(XhtmlNodeList children, List<ElementDefinitionBindingAdditionalComponent> list) { 387 if (list.size() == 1) { 388 render(children, list.get(0)); 389 } else { 390 XhtmlNode ul = children.ul(); 391 for (ElementDefinitionBindingAdditionalComponent b : list) { 392 render(ul.li().getChildNodes(), b); 393 } 394 } 395 } 396 397 private void render(XhtmlNodeList children, ElementDefinitionBindingAdditionalComponent b) { 398 if (b.getValueSet() == null) { 399 return; // what should happen? 400 } 401 BindingResolution br = pkp.resolveBinding(profile, b.getValueSet(), corePath); 402 XhtmlNode a = children.ahOrCode(br.url == null ? null : Utilities.isAbsoluteUrl(br.url) || !context.getPkp().prependLinks() ? br.url : corePath+br.url, b.hasDocumentation() ? b.getDocumentation() : null); 403 if (b.hasDocumentation()) { 404 a.attribute("title", b.getDocumentation()); 405 } 406 a.tx(br.display); 407 408 if (b.hasShortDoco()) { 409 children.tx(": "); 410 children.tx(b.getShortDoco()); 411 } 412 if (b.getAny() || b.hasUsage()) { 413 children.tx(" ("); 414 boolean ffirst = !b.getAny(); 415 if (b.getAny()) { 416 children.tx("any repeat"); 417 } 418 for (UsageContext uc : b.getUsage()) { 419 if (ffirst) ffirst = false; else children.tx(","); 420 if (!uc.getCode().is("http://terminology.hl7.org/CodeSystem/usage-context-type", "jurisdiction")) { 421 children.tx(displayForUsage(uc.getCode())); 422 children.tx("="); 423 } 424 CodeResolution ccr = cr.resolveCode(uc.getValueCodeableConcept()); 425 children.ah(ccr.getLink(), ccr.getHint()).tx(ccr.getDisplay()); 426 } 427 children.tx(")"); 428 } 429 } 430 431 432 private String displayForUsage(Coding c) { 433 if (c.hasDisplay()) { 434 return c.getDisplay(); 435 } 436 if ("http://terminology.hl7.org/CodeSystem/usage-context-type".equals(c.getSystem())) { 437 return c.getCode(); 438 } 439 return c.getCode(); 440 } 441 442 public void seeAdditionalBinding(String purpose, String doco, ValueSet valueSet) { 443 AdditionalBindingDetail abr = new AdditionalBindingDetail(); 444 abr.purpose = purpose; 445 abr.valueSet = valueSet.getUrl(); 446 abr.vs = valueSet; 447 bindings.add(abr); 448 } 449 450 public void seeAdditionalBinding(String purpose, String doco, String ref) { 451 AdditionalBindingDetail abr = new AdditionalBindingDetail(); 452 abr.purpose = purpose; 453 abr.valueSet = ref; 454 bindings.add(abr); 455 456 } 457 458}