001package org.hl7.fhir.r5.conformance; 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.renderers.CodeResolver; 023import org.hl7.fhir.r5.renderers.CodeResolver.CodeResolution; 024import org.hl7.fhir.r5.renderers.DataRenderer; 025import org.hl7.fhir.r5.renderers.IMarkdownProcessor; 026import org.hl7.fhir.r5.renderers.utils.RenderingContext; 027import org.hl7.fhir.r5.utils.PublicationHacker; 028import org.hl7.fhir.r5.utils.ToolingExtensions; 029import org.hl7.fhir.utilities.MarkDownProcessor; 030import org.hl7.fhir.utilities.Utilities; 031import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator; 032import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Cell; 033import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Piece; 034import org.hl7.fhir.utilities.xhtml.NodeType; 035import org.hl7.fhir.utilities.xhtml.XhtmlComposer; 036import org.hl7.fhir.utilities.xhtml.XhtmlNode; 037import org.hl7.fhir.utilities.xhtml.XhtmlNodeList; 038 039public class AdditionalBindingsRenderer { 040 public class AdditionalBindingDetail { 041 private String purpose; 042 private String valueSet; 043 private String doco; 044 private String docoShort; 045 private UsageContext usage; 046 private boolean any = false; 047 private boolean isUnchanged = false; 048 private boolean matched = false; 049 private boolean removed = false; 050 private AdditionalBindingDetail compare; 051 private int count = 1; 052 private String getKey() { 053 // Todo: Consider extending this with content from usageContext if purpose isn't sufficiently differentiating 054 return purpose + Integer.toString(count); 055 } 056 private void incrementCount() { 057 count++; 058 } 059 private void setCompare(AdditionalBindingDetail match) { 060 compare = match; 061 match.matched = true; 062 } 063 private boolean alreadyMatched() { 064 return matched; 065 } 066 public String getDoco(boolean full) { 067 return full ? doco : docoShort; 068 } 069 public boolean unchanged() { 070 if (!isUnchanged) 071 return false; 072 if (compare==null) 073 return true; 074 isUnchanged = true; 075 isUnchanged = isUnchanged && ((purpose==null && compare.purpose==null) || purpose.equals(compare.purpose)); 076 isUnchanged = isUnchanged && ((valueSet==null && compare.valueSet==null) || valueSet.equals(compare.valueSet)); 077 isUnchanged = isUnchanged && ((doco==null && compare.doco==null) || doco.equals(compare.doco)); 078 isUnchanged = isUnchanged && ((usage==null && compare.usage==null) || usage.equals(compare.usage)); 079 return isUnchanged; 080 } 081 } 082 083 private static String STYLE_UNCHANGED = "opacity: 0.5;"; 084 private static String STYLE_REMOVED = STYLE_UNCHANGED + "text-decoration: line-through;"; 085 086 private List<AdditionalBindingDetail> bindings = new ArrayList<>(); 087 private ProfileKnowledgeProvider pkp; 088 private String corePath; 089 private StructureDefinition profile; 090 private String path; 091 private RenderingContext context; 092 private IMarkdownProcessor md; 093 private CodeResolver cr; 094 095 public AdditionalBindingsRenderer(ProfileKnowledgeProvider pkp, String corePath, StructureDefinition profile, String path, RenderingContext context, IMarkdownProcessor md, CodeResolver cr) { 096 this.pkp = pkp; 097 this.corePath = corePath; 098 this.profile = profile; 099 this.path = path; 100 this.context = context; 101 this.md = md; 102 this.cr = cr; 103 } 104 105 public void seeMaxBinding(Extension ext) { 106 seeMaxBinding(ext, null, false); 107 } 108 109 public void seeMaxBinding(Extension ext, Extension compExt, boolean compare) { 110 seeBinding(ext, compExt, compare, "maximum"); 111 } 112 113 protected void seeBinding(Extension ext, Extension compExt, boolean compare, String label) { 114 AdditionalBindingDetail abr = new AdditionalBindingDetail(); 115 abr.purpose = label; 116 abr.valueSet = ext.getValue().primitiveValue(); 117 if (compare) { 118 abr.isUnchanged = compExt!=null && ext.getValue().primitiveValue().equals(compExt.getValue().primitiveValue()); 119 120 abr.compare = new AdditionalBindingDetail(); 121 abr.compare.valueSet = compExt==null ? null : compExt.getValue().primitiveValue(); 122 } else { 123 abr.isUnchanged = ext.hasUserData(ProfileUtilities.UD_DERIVATION_EQUALS); 124 } 125 bindings.add(abr); 126 } 127 128 public void seeMinBinding(Extension ext) { 129 seeMinBinding(ext, null, false); 130 } 131 132 public void seeMinBinding(Extension ext, Extension compExt, boolean compare) { 133 seeBinding(ext, compExt, compare, "minimum"); 134 } 135 136 public void seeAdditionalBindings(List<Extension> list) { 137 seeAdditionalBindings(list, null, false); 138 } 139 140 public void seeAdditionalBindings(List<Extension> list, List<Extension> compList, boolean compare) { 141 HashMap<String, AdditionalBindingDetail> compBindings = new HashMap<String, AdditionalBindingDetail>(); 142 if (compare && compList!=null) { 143 for (Extension ext : compList) { 144 AdditionalBindingDetail abr = additionalBinding(ext); 145 if (compBindings.containsKey(abr.getKey())) { 146 abr.incrementCount(); 147 } 148 compBindings.put(abr.getKey(), abr); 149 } 150 } 151 152 for (Extension ext : list) { 153 AdditionalBindingDetail abr = additionalBinding(ext); 154 if (compare && compList!=null) { 155 AdditionalBindingDetail match = null; 156 do { 157 match = compBindings.get(abr.getKey()); 158 if (abr.alreadyMatched()) 159 abr.incrementCount(); 160 } while (match!=null && abr.alreadyMatched()); 161 if (match!=null) 162 abr.setCompare(match); 163 bindings.add(abr); 164 if (abr.compare!=null) 165 compBindings.remove(abr.compare.getKey()); 166 } else 167 bindings.add(abr); 168 } 169 for (AdditionalBindingDetail b: compBindings.values()) { 170 b.removed = true; 171 bindings.add(b); 172 } 173 } 174 175 protected AdditionalBindingDetail additionalBinding(Extension ext) { 176 AdditionalBindingDetail abr = new AdditionalBindingDetail(); 177 abr.purpose = ext.getExtensionString("purpose"); 178 abr.valueSet = ext.getExtensionString("valueSet"); 179 abr.doco = ext.getExtensionString("documentation"); 180 abr.docoShort = ext.getExtensionString("shortDoco"); 181 abr.usage = (ext.hasExtension("usage")) && ext.getExtensionByUrl("usage").hasValueUsageContext() ? ext.getExtensionByUrl("usage").getValueUsageContext() : null; 182 abr.any = "any".equals(ext.getExtensionString("scope")); 183 abr.isUnchanged = ext.hasUserData(ProfileUtilities.UD_DERIVATION_EQUALS); 184 return abr; 185 } 186 187 public String render() throws IOException { 188 if (bindings.isEmpty()) { 189 return ""; 190 } else { 191 XhtmlNode tbl = new XhtmlNode(NodeType.Element, "table"); 192 tbl.attribute("class", "grid"); 193 render(tbl.getChildNodes(), true); 194 return new XhtmlComposer(false).compose(tbl); 195 } 196 } 197 198 public void render(HierarchicalTableGenerator gen, Cell c) throws FHIRFormatError, DefinitionException, IOException { 199 if (bindings.isEmpty()) { 200 return; 201 } else { 202 Piece piece = gen.new Piece("table").attr("class", "grid"); 203 c.getPieces().add(piece); 204 render(piece.getChildren(), false); 205 } 206 } 207 208 public void render(List<XhtmlNode> children, boolean fullDoco) throws FHIRFormatError, DefinitionException, IOException { 209 boolean doco = false; 210 boolean usage = false; 211 boolean any = false; 212 for (AdditionalBindingDetail binding : bindings) { 213 doco = doco || binding.getDoco(fullDoco)!=null || (binding.compare!=null && binding.compare.getDoco(fullDoco)!=null); 214 usage = usage || binding.usage != null || (binding.compare!=null && binding.compare.usage!=null); 215 any = any || binding.any || (binding.compare!=null && binding.compare.any); 216 } 217 218 XhtmlNode tr = new XhtmlNode(NodeType.Element, "tr"); 219 children.add(tr); 220 tr.td().style("font-size: 11px").b().tx("Additional Bindings"); 221 tr.td().style("font-size: 11px").tx("Purpose"); 222 if (usage) { 223 tr.td().style("font-size: 11px").tx("Usage"); 224 } 225 if (any) { 226 tr.td().style("font-size: 11px").tx("Any"); 227 } 228 if (doco) { 229 tr.td().style("font-size: 11px").tx("Documentation"); 230 } 231 for (AdditionalBindingDetail binding : bindings) { 232 tr = new XhtmlNode(NodeType.Element, "tr"); 233 if (binding.unchanged()) { 234 tr.style(STYLE_REMOVED); 235 } else if (binding.removed) { 236 tr.style(STYLE_REMOVED); 237 } 238 children.add(tr); 239 BindingResolution br = pkp == null ? makeNullBr(binding) : pkp.resolveBinding(profile, binding.valueSet, path); 240 BindingResolution compBr = null; 241 if (binding.compare!=null && binding.compare.valueSet!=null) 242 compBr = pkp == null ? makeNullBr(binding.compare) : pkp.resolveBinding(profile, binding.compare.valueSet, path); 243 244 XhtmlNode valueset = tr.td().style("font-size: 11px"); 245 if (binding.compare!=null && binding.valueSet.equals(binding.compare.valueSet)) 246 valueset.style(STYLE_UNCHANGED); 247 if (br.url != null) { 248 valueset.ah(determineUrl(br.url), binding.valueSet).tx(br.display); 249 } else { 250 valueset.span(null, binding.valueSet).tx(br.display); 251 } 252 if (binding.compare!=null && binding.compare.valueSet!=null && !binding.valueSet.equals(binding.compare.valueSet)) { 253 valueset.br(); 254 valueset = valueset.span(STYLE_REMOVED, null); 255 if (compBr.url != null) { 256 valueset.ah(determineUrl(compBr.url), binding.compare.valueSet).tx(compBr.display); 257 } else { 258 valueset.span(null, binding.compare.valueSet).tx(compBr.display); 259 } 260 } 261 262 XhtmlNode purpose = tr.td().style("font-size: 11px"); 263 if (binding.compare!=null && binding.purpose.equals(binding.compare.purpose)) 264 purpose.style("font-color: darkgray"); 265 renderPurpose(purpose, binding.purpose); 266 if (binding.compare!=null && binding.compare.purpose!=null && !binding.purpose.equals(binding.compare.purpose)) { 267 purpose.br(); 268 purpose = purpose.span(STYLE_UNCHANGED, null); 269 renderPurpose(purpose, binding.compare.purpose); 270 } 271 if (usage) { 272 if (binding.usage != null) { 273 // TODO: This isn't rendered at all yet. Ideally, we want it to render with comparison... 274 new DataRenderer(context).render(tr.td(), binding.usage); 275 } else { 276 tr.td(); 277 } 278 } 279 if (any) { 280 String newRepeat = binding.any ? "Any repeats" : "All repeats"; 281 String oldRepeat = binding.compare!=null && binding.compare.any ? "Any repeats" : "All repeats"; 282 compareString(tr.td().style("font-size: 11px"), newRepeat, oldRepeat); 283 } 284 if (doco) { 285 if (binding.doco != null) { 286 String d = fullDoco ? md.processMarkdown("Binding.description", binding.doco) : binding.docoShort; 287 String oldD = binding.compare==null ? null : fullDoco ? md.processMarkdown("Binding.description.compare", binding.compare.doco) : binding.compare.docoShort; 288 tr.td().style("font-size: 11px").innerHTML(compareHtml(d, oldD)); 289 } else { 290 tr.td().style("font-size: 11px"); 291 } 292 } 293 } 294 } 295 296 private XhtmlNode compareString(XhtmlNode node, String newS, String oldS) { 297 if (oldS==null) 298 return node.tx(newS); 299 if (newS.equals(oldS)) 300 return node.style(STYLE_UNCHANGED).tx(newS); 301 node.tx(newS); 302 node.br(); 303 return node.span(STYLE_REMOVED,null).tx(oldS); 304 } 305 306 private String compareHtml(String newS, String oldS) { 307 if (oldS==null) 308 return newS; 309 if (newS.equals(oldS)) 310 return "<span style=\"" + STYLE_UNCHANGED + "\">" + newS + "</span>"; 311 return newS + "<br/><span style=\"" + STYLE_REMOVED + "\">" + oldS + "</span>"; 312 } 313 314 private String determineUrl(String url) { 315 return Utilities.isAbsoluteUrl(url) || !pkp.prependLinks() ? url : corePath + url; 316 } 317 318 private void renderPurpose(XhtmlNode td, String purpose) { 319 switch (purpose) { 320 case "maximum": 321 td.ah(corePath+"extension-elementdefinition-maxvalueset.html", "A required binding, for use when the binding strength is 'extensible' or 'preferred'").tx("Max Binding"); 322 break; 323 case "minimum": 324 td.ah(corePath+"extension-elementdefinition-minvalueset.html", "The minimum allowable value set - any conformant system SHALL support all these codes").tx("Min Binding"); 325 break; 326 case "required" : 327 td.ah(corePath+"terminologies.html#strength", "Validators will check this binding (strength = required)").tx("Validation Binding"); 328 break; 329 case "extensible" : 330 td.ah(corePath+"terminologies.html#strength", "Validators will check this binding (strength = extensible)").tx("Validation Binding"); 331 break; 332 case "candidate" : 333 td.ah(corePath+"terminologies.html#strength", "This is a candidate binding that constraints on this profile may consider (see doco)").tx("Candidate Validation Binding"); 334 break; 335 case "current" : 336 td.span(null, "New records are required to use this value set, but legacy records may use other codes").tx("Required"); 337 break; 338 case "preferred" : 339 td.span(null, "This is the value set that is recommended (documentation should explain why)").tx("Recommended"); 340 break; 341 case "ui" : 342 td.span(null, "This value set is provided to user look up in a given context").tx("UI"); 343 break; 344 case "starter" : 345 td.span(null, "This value set is a good set of codes to start with when designing your system").tx("Starter"); 346 break; 347 case "component" : 348 td.span(null, "This value set is a component of the base value set").tx("Component"); 349 break; 350 default: 351 td.span(null, "Unknown code for purpose").tx(purpose); 352 } 353 } 354 355 private BindingResolution makeNullBr(AdditionalBindingDetail binding) { 356 BindingResolution br = new BindingResolution(); 357 br.url = "http://none.none/none"; 358 br.display = "todo"; 359 return br; 360 } 361 362 public boolean hasBindings() { 363 return !bindings.isEmpty(); 364 } 365 366 public void render(XhtmlNodeList children, List<ElementDefinitionBindingAdditionalComponent> list) { 367 if (list.size() == 1) { 368 render(children, list.get(0)); 369 } else { 370 XhtmlNode ul = children.ul(); 371 for (ElementDefinitionBindingAdditionalComponent b : list) { 372 render(ul.li().getChildNodes(), b); 373 } 374 } 375 } 376 377 private void render(XhtmlNodeList children, ElementDefinitionBindingAdditionalComponent b) { 378 if (b.getValueSet() == null) { 379 return; // what should happen? 380 } 381 BindingResolution br = pkp.resolveBinding(profile, b.getValueSet(), corePath); 382 XhtmlNode a = children.ah(br.url == null ? null : Utilities.isAbsoluteUrl(br.url) || !context.getPkp().prependLinks() ? br.url : corePath+br.url, b.hasDocumentation() ? b.getDocumentation() : null); 383 if (b.hasDocumentation()) { 384 a.attribute("title", b.getDocumentation()); 385 } 386 a.tx(br.display); 387 388 if (b.hasShortDoco()) { 389 children.tx(": "); 390 children.tx(b.getShortDoco()); 391 } 392 if (b.getAny() || b.hasUsage()) { 393 children.tx(" ("); 394 boolean ffirst = !b.getAny(); 395 if (b.getAny()) { 396 children.tx("any repeat"); 397 } 398 for (UsageContext uc : b.getUsage()) { 399 if (ffirst) ffirst = false; else children.tx(","); 400 if (!uc.getCode().is("http://terminology.hl7.org/CodeSystem/usage-context-type", "jurisdiction")) { 401 children.tx(displayForUsage(uc.getCode())); 402 children.tx("="); 403 } 404 CodeResolution ccr = cr.resolveCode(uc.getValueCodeableConcept()); 405 children.ah(ccr.getLink(), ccr.getHint()).tx(ccr.getDisplay()); 406 } 407 children.tx(")"); 408 } 409 } 410 411 412 private String displayForUsage(Coding c) { 413 if (c.hasDisplay()) { 414 return c.getDisplay(); 415 } 416 if ("http://terminology.hl7.org/CodeSystem/usage-context-type".equals(c.getSystem())) { 417 return c.getCode(); 418 } 419 return c.getCode(); 420 } 421 422}